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 01/53] 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 784cb4d3dbe7f8248186e8f5948fa2fc98aef731 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:00:30 +0200 Subject: [PATCH 02/53] 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 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 03/53] 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 04/53] 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 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 05/53] 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 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 06/53] 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 07/53] 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 08/53] 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 09/53] 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 10/53] 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 11/53] 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 12/53] 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 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 13/53] 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 9800f3ab92c058e649c50c771a131fe641106d96 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Thu, 15 Oct 2026 12:45:54 -0400 Subject: [PATCH 14/53] 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 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 15/53] 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 2132abc6988ab4500a58bc612c977a87038ab394 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Fri, 16 Oct 2026 09:17:24 -0400 Subject: [PATCH 16/53] 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 6dbbf4d7d9bb27f6f5cbbed9e24934b5d8ec5452 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 21:41:54 +0000 Subject: [PATCH 17/53] 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 18/53] 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 19/53] 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 20/53] 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 21/53] 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 22/53] 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 9dc65429991c7619e13c7827867063c6811aba50 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Thu, 15 Oct 2026 23:18:14 +0200 Subject: [PATCH 23/53] 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 b808658488ad2ae4022da877f27afbd577c03202 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Sat, 17 Oct 2026 16:35:15 +0200 Subject: [PATCH 24/53] 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 d915e5835a6d7c1fda2f53e216a45fbc4a3d8c60 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Sat, 17 Oct 2026 16:41:22 +0200 Subject: [PATCH 25/53] 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 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 26/53] 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 2578949658293847db4357c57ede28fbeb7f8c21 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 14:46:37 +0000 Subject: [PATCH 27/53] 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 6ce6850d12a8b55d80381095ca23736f9dd7116c Mon Sep 17 00:00:00 2001 From: simonbronk Date: Sat, 17 Oct 2026 17:23:32 +0200 Subject: [PATCH 28/53] 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 6e0886d76d0300960139ee806bbee7b2167f206d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 16:32:08 +0000 Subject: [PATCH 29/53] 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 227d1e44a10edadbec0373339cc4b3fea888e167 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 17:03:09 +0000 Subject: [PATCH 30/53] 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 70ff799fdbd526b1d27bd7a976b0dd92a1e1f7fc Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Sat, 17 Oct 2026 16:17:05 -0500 Subject: [PATCH 31/53] 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 32/53] 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 33/53] 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 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 34/53] 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 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 35/53] 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 36/53] 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 37/53] 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 38/53] 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 39/53] 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 40/53] 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 41/53] 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 42/53] 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 4d741c26de4329c8391811dc76c3f7dadd9395ed Mon Sep 17 00:00:00 2001 From: MontyMcK Date: Sun, 18 Oct 2026 12:30:43 +1300 Subject: [PATCH 43/53] 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 -#include -#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 +#include +#include namespace { -constexpr std::int32_t SCE_HMD2_ERROR_UNSUPPORTED_FEATURE = static_cast(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(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 +#include + +namespace Hmd2 { + +inline constexpr std::int32_t AlreadyInitialized = static_cast(0x8a720001); +inline constexpr std::int32_t NotInitialized = static_cast(0x8a720002); +inline constexpr std::int32_t InvalidHandle = static_cast(0x8a720003); +inline constexpr std::int32_t AlreadyOpened = static_cast(0x8a720006); +inline constexpr std::int32_t ParameterNull = static_cast(0x8a720008); +inline constexpr std::int32_t ParameterInvalid = static_cast(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 #include #include #include +#include #include -extern "C" { -std::int32_t APS5_VABI sceHmd2Initialize(const void* param); -int APS5_VABI sceHmd2Open(); -} - namespace { +alignas(16) std::array 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(0x81110016), "initialization did not report the unsupported feature"); - Require(sceHmd2Initialize(nullptr) == static_cast(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 +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__linux__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include +#include +#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(GetModuleHandleW(nullptr)); + if (image == nullptr) return false; + auto* dos = reinterpret_cast(image); + auto* nt = reinterpret_cast(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(process - image); + metadata[1] = static_cast(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(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(&header); + const auto pageSize = static_cast(sysconf(_SC_PAGESIZE)); + const auto pageStart = reinterpret_cast(mutableHeader) & ~(pageSize - 1); + const auto pageEnd = (reinterpret_cast(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(pageStart), pageEnd - pageStart, protection | PROT_WRITE) != 0) return 1; + mutableHeader->p_type = 0x61000001; + mutableHeader->p_vaddr = reinterpret_cast(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(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 316ad40b46167e726c6c3619e5bfae3c6d426ce0 Mon Sep 17 00:00:00 2001 From: Kaua Date: Sat, 17 Oct 2026 21:06:51 -0300 Subject: [PATCH 44/53] 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 45/53] 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_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel) + 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/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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(0x81581000u); +constexpr int OutOfRange = static_cast(0x81580000u); +constexpr int NullArgument = static_cast(0x81580001u); +constexpr int NoNodes = static_cast(0x81580002u); +constexpr int LimitExceeded = static_cast(0x81580003u); +constexpr int NoneFree = static_cast(0x81580006u); +constexpr int Invalid = static_cast(0x81580007u); + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array Value{}; +alignas(256) std::array Table{}; +alignas(256) volatile std::uint32_t Output[4]{}; +alignas(256) std::array OutputDescriptor{}; +alignas(32) std::array IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0}; +alignas(16) std::array Specials{}; +alignas(16) std::array SpecialsWithoutDispatch{}; +std::array Registers{}; +alignas(16) std::array Shader{}; +alignas(16) std::array ShaderWithoutDispatch{}; + +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); +} + +void BuildShader() { + Table = {Low(Value.data()), High(Value.data())}; + OutputDescriptor = {Low(Output), High(Output) & 0xffffu, sizeof(Output), 0x31016facu}; + const auto code = static_cast(reinterpret_cast(ReaderCode.data())); + Registers = {{{0x20c, static_cast(code >> 8u)}, {0x20d, static_cast(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(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 Released{false}; +std::atomic 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(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 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& guard); + int AppendCompute(Item item); + void SubmitCompute(const Item& item); Counters limits; Counters submitted; @@ -172,6 +199,11 @@ private: std::deque 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> commands; }; class Factory final : public ISequenceFactory { @@ -196,6 +228,11 @@ struct Manager { std::uint32_t persistentThreads = 0; std::array, 2> runQueues; std::vector sequences; + std::deque computeSequences; + std::uint32_t freeComputeSequences = 0; + std::vector computeQueues; + KernelEqueue computeEvents = 0; + Pthread computeHandler = nullptr; std::array factories; std::array unifiedFactories; std::vector 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(cursor) + 1; items.erase(items.begin(), items.begin() + static_cast(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(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(item.argument); + const std::array userDataWords{static_cast(userData), static_cast(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(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(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(x), static_cast(y), static_cast(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(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(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 +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(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(index)]; + if (!sequence.Open(manager, limits, priority)) return SequenceNoNodes; *out = &sequence; return 0; } -int Factory::Destroy(ISequence* sequence) { +template +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(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(argument); + for (;;) { + KernelEvent event{}; + int count = 0; + static_cast(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& 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(((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::countr_zero(mask)); + if (sceAgcDriverAddEqEvent(manager.computeEvents, static_cast(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(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(StateOffset), 0x05000001, static_cast(size), 0}; std::memcpy(memory, &header, sizeof(header)); if (config != nullptr && config->version > 1) return ManagerInvalidArgument; - if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::initialize with compute sequences"); const ConfigParams& settings = config != nullptr ? *config : DefaultConfig; + const auto computeQueues = computeSequences != 0 ? static_cast(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::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(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(sceAgcDriverDeleteEqEvent(manager->computeEvents, static_cast(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 46/53] 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(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::popcount(computeQueueMask)) < computeSequences) return ManagerInvalidArgument; + if ((sequences == 0 && computeSequences == 0) || std::uint64_t{sequences} + computeSequences > MaxSequences || (computeQueueMask >> 56) != 0 || static_cast(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 Date: Sun, 18 Oct 2026 13:20:55 +1300 Subject: [PATCH 47/53] 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 48/53] 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 6c47f92b52a70d8f39877b1261f564d9213a3ae8 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 00:43:33 +0000 Subject: [PATCH 49/53] 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 --- .../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(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(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 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 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 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 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(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 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 Date: Sun, 18 Oct 2026 00:45:12 +0000 Subject: [PATCH 50/53] 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 #include #include +#include #include #ifdef _WIN32 #include @@ -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 Date: Sun, 18 Oct 2026 07:43:54 +0700 Subject: [PATCH 51/53] 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 --- 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 #include +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 recording{false}; + static std::atomic recording; inline static std::atomic buffering{false}; inline static std::atomic presentCount{0}; - inline static thread_local TraceThread* local = nullptr; + static thread_local TraceThread* local; inline static std::mutex gpuMutex; inline static std::vector 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 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 Date: Sun, 18 Oct 2026 01:00:16 +0000 Subject: [PATCH 52/53] 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 --- 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("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 (...) { 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("vkGetPhysicalDeviceMemoryProperties2"); 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 (...) { 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("vkGetPhysicalDeviceFormatProperties"); 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 (...) { 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("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 (...) { 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("vkGetPhysicalDeviceFormatProperties"); 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 (...) { 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("vkGetPhysicalDeviceMemoryProperties2"); 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 (...) { 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("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); 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 (...) { From f96cdc1e7eb872466241b45554f069786677d443 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 01:10:16 +0000 Subject: [PATCH 53/53] 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); }