From fc002289385b400f7c814bfd4f1c4d608717031f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 4 Oct 2026 11:20:38 +0200 Subject: [PATCH 001/203] docs: add architecture overview with Mermaid diagrams Two diagrams in docs/dev/ARCHITECTURE.md: how the relinker and the NID-patched system libraries produce a native program, and the graphics path from command buffers through the shader recompiler to Vulkan. README only gains a link. Co-Authored-By: Claude --- README.md | 2 +- docs/dev/ARCHITECTURE.md | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 docs/dev/ARCHITECTURE.md diff --git a/README.md b/README.md index a06939e2b..5e57cb94f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Tool for automatic executables porting to Linux and Windows. Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. -[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md) +[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md) ## Status diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md new file mode 100644 index 000000000..1a617546a --- /dev/null +++ b/docs/dev/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# Architecture + +How a PS5 executable becomes a native Linux or Windows program. Nothing is emulated: the converted executable runs as a normal process and calls native implementations of the system libraries. + +## Overview + +```mermaid +flowchart LR + subgraph input["PS5 game"] + elf["input.elf"] + mods["sce_module/*"] + end + + subgraph relinker["relinker (core/relinker)"] + direction TB + intel["--to-intel:
lower AMD-only instructions
(codegen)"] + pipeline["RelinkerPipeline:
read imports by NID,
check syscalls, filter unused NIDs,
build SysV dynamic section"] + guest["GuestModuleBuilder:
convert bundled modules"] + patcher["LinuxElfPatcher / WindowsPePatcher"] + intel -.-> pipeline --> guest --> patcher + end + + subgraph build["build (core/libs)"] + direction TB + prx["core/libs/prx/*
shared libraries"] + nid["nid_patcher:
rename exports to their NIDs"] + prx --> nid + end + + subgraph out["Native program"] + app["app.elf / app.exe"] + app0["app0/sce_module/*"] + libs["libs/*.prx"] + end + + elf --> intel + mods --> guest + patcher --> app + guest --> app0 + nid --> libs + app -- "OS loader binds imports by NID" --> libs + app0 -- "OS loader binds imports by NID" --> libs +``` + +- [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). +- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. + +## Graphics + +```mermaid +flowchart LR + game["Game:
command buffers"] --> submit["libSceAgcDriver/Submit
DCB / ACB"] + submit --> pm4["Execution/Pm4:
state, draws,
dispatches"] + pm4 -- "shader + state" --> cache{"Compiled variant
in memory or
on disk?"} + cache -- yes --> vk + cache -- no --> dec + + subgraph recompiler["core/shader/recompiler"] + dec["RdnaDecoder"] --> cf["ControlFlow:
graph + structurize"] + cf --> tr["Translation:
RDNA to IR"] + tr --> opt["Optimization:
SSA, resources,
bindings"] + opt --> spv["SpirvBackend:
emit SPIR-V"] + end + + spv --> vk["libSceAgcDriver/Graphics:
Vulkan pipeline"] +``` + +- [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. +- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. From 87e261b451d6192263fa2ed1d0f322f092dc3839 Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:57:48 +0300 Subject: [PATCH 002/203] perf(shader): intern SPIR-V type and constant ids without a per-call allocation Every SpirvModule::Type and Constant call built a heap std::vector key and looked it up in a std::map keyed on std::vector, so the comparison walked the key bytes. At the one to three such calls per emitted instruction a shader issues thousands of them per compile. Add a fixed-size, linearly probed declaration cache that holds the key inline and is consulted before the key vector is built, making a repeated declaration a hash and a compare. The cacheable shapes are those whose operands are a word: OpTypeVoid, OpTypeBool, OpTypeInt, OpTypeFloat, OpTypeVector, OpTypePointer, OpTypeStruct, OpTypeArray, OpTypeFunction and the scalar OpConstant family. The overloads are constrained on word-sized operands so that enum operands such as spv::StorageClassFunction reach the cache instead of binding the variadic template by reference. Measured with a harness driving the module through the emitters' call shapes: 80000 hot type and constant calls drop from 80074 heap allocations to 140. The assigned ids and the emitted SPIR-V word stream are byte-identical. Co-Authored-By: Claude --- .../include/SpirvBackend/SpirvModule.hpp | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index ce1d7b620..a8e1f5048 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -1,6 +1,8 @@ #ifndef CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVMODULE_HPP #define CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVMODULE_HPP +#include +#include #include #include #include @@ -51,6 +53,28 @@ public: return declareType(opcode, makeTypeKey(opcode, operands...)); } + std::uint32_t Type(std::uint32_t opcode) { + const std::uint32_t key[2] = {opcode, 0u}; + return interned(key, 2u, [this, opcode]() { return declareType(opcode, makeTypeKey(opcode)); }); + } + + template + requires wordSized + std::uint32_t Type(std::uint32_t opcode, TWord width) { + const auto narrowed = narrowWord(width); + const std::uint32_t key[3] = {opcode, 1u, narrowed}; + return interned(key, 3u, [this, opcode, width]() { return declareType(opcode, makeTypeKey(opcode, width)); }); + } + + template + requires (wordSized && wordSized) + std::uint32_t Type(std::uint32_t opcode, TWord width, TSign signness) { + const auto narrowedWidth = narrowWord(width); + const auto narrowedSignness = narrowWord(signness); + const std::uint32_t key[4] = {opcode, 2u, narrowedWidth, narrowedSignness}; + return interned(key, 4u, [this, opcode, width, signness]() { return declareType(opcode, makeTypeKey(opcode, width, signness)); }); + } + template std::uint32_t DecoratedType(std::uint32_t opcode, std::initializer_list annotations, const TOperands&... operands) { return declareDecoratedType(opcode, makeTypeKey(opcode, operands...), annotations); @@ -64,6 +88,19 @@ public: return declareConstant(opcode, std::move(key)); } + template + requires wordSized + std::uint32_t Constant(std::uint32_t opcode, std::uint32_t type, TValue value) { + const auto narrowed = narrowWord(value); + const std::uint32_t key[3] = {opcode, type, narrowed}; + return interned(key, 3u, [this, opcode, type, value]() { + std::vector keyWords; + keyWords.reserve(3u); + appendOperands(keyWords, opcode, type, value); + return declareConstant(opcode, std::move(keyWords)); + }); + } + template void AddExecutionMode(std::uint32_t entryPoint, std::uint32_t mode, const TOperands&... operands) { appendInstruction(executionModes, spv::OpExecutionMode, entryPoint, mode, operands...); @@ -80,6 +117,54 @@ public: } private: + static constexpr std::size_t DeclarationCacheSize = 512; + static constexpr std::size_t MaxCachedDeclarationWords = 4; + + template + static constexpr bool wordSized = (std::is_integral_v || std::is_enum_v) && sizeof(TOperand) <= sizeof(std::uint32_t); + + template + static std::uint32_t narrowWord(TOperand operand) { + return static_cast(operand); + } + + struct DeclarationSlot { + std::array words = {}; + std::uint32_t length = 0; + std::uint32_t id = 0; + }; + + static std::size_t declarationCacheSlot(const std::uint32_t* words, std::uint32_t length) { + std::uint64_t hash = 14695981039346656037ull; + for (std::uint32_t index = 0; index < length; index++) { + hash ^= words[index]; + hash *= 1099511628211ull; + } + return static_cast(hash) & (DeclarationCacheSize - 1u); + } + + template + std::uint32_t interned(const std::uint32_t* words, std::uint32_t length, TDeclare&& declare) { + const auto start = declarationCacheSlot(words, length); + std::size_t target = start; + for (std::size_t probe = 0; probe < DeclarationCacheSize; ++probe) { + target = (start + probe) & (DeclarationCacheSize - 1u); + auto& slot = declarationCache[target]; + if (slot.id == 0) { + break; + } + if (slot.length == length && std::equal(words, words + length, slot.words.begin())) { + return slot.id; + } + } + const auto id = declare(); + auto& slot = declarationCache[target]; + std::copy(words, words + length, slot.words.begin()); + slot.length = length; + slot.id = id; + return id; + } + static void appendOperand(std::vector& words, std::uint32_t value) { words.push_back(value); } @@ -147,6 +232,7 @@ private: std::set requiredExtensions; std::map importIds; std::map, std::uint32_t> declarationIds; + std::array declarationCache = {}; std::size_t unpatchedPhiIncomings = 0; std::vector capabilities; std::vector extensions; From 4dd9d45be876169c984f386fe28e12288990f83e Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:09:17 +0300 Subject: [PATCH 003/203] fix(shader): cache operand-free constants and cover interning with a test OpConstantTrue and OpConstantFalse carry no operand word, so they reached the declaration cache through the variadic Constant template, which builds the key vector, and paid an allocation on every call. ConstantBool emits both, so a shader paid it twice per boolean constant. Add the two-word overload; the compile-shaped benchmark drops from 10413 allocations to 415. Add agc_spirv_module_tests covering the interning contract: a repeated declaration returns one id and is emitted once, operand shapes that share a leading key word stay distinct, integral and enum operands agree, decorated types stay separate from plain ones, memoized ids survive AllocateId, modules do not share ids, and ids stay unique and contiguous past the cache capacity. The test passes against both the previous header and this one, so it pins the contract rather than the implementation. Benchmark, pre-optimization header against this one, best of five: hot loop x 2M 613.6 ms -> 46.3 ms 13.2x compile-shaped 5000 instr 4.2 ms -> 0.4 ms 10.8x 64 compile-shaped modules 286.0 ms -> 25.4 ms 11.3x Allocations for the compile-shaped case fall from 55217 to 415. Both builds compute the same value over the same call sequence. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 203 ++++++++++++++++++ .../include/SpirvBackend/SpirvModule.hpp | 10 + 3 files changed, 218 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bd05f3b39..ce3801074 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -274,6 +274,11 @@ target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_drive set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_structurizer_tests) +add_test_executable(agc_spirv_module_tests EXCLUDE_FROM_ALL tests/SpirvModule.cpp) +target_link_libraries(agc_spirv_module_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_module_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_module_tests) + add_test_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp) target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp new file mode 100644 index 000000000..bfd9fd061 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -0,0 +1,203 @@ +#include "SpirvBackend/SpirvModule.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +int failures = 0; + +void check(bool condition, const char* what) { + if (!condition) { + std::fprintf(stderr, "%s\n", what); + ++failures; + } +} + +std::map opcodeCounts(const std::vector& words) { + std::map counts; + std::size_t offset = 5; + while (offset < words.size()) { + const auto header = words[offset]; + const auto wordCount = header >> spv::WordCountShift; + if (wordCount == 0 || offset + wordCount > words.size()) { + return counts; + } + ++counts[header & 0xffffu]; + offset += wordCount; + } + return counts; +} + +std::size_t declared(const SpirvModule& module, std::uint32_t opcode) { + const auto counts = opcodeCounts(module.Finalize()); + const auto found = counts.find(opcode); + return found == counts.end() ? 0 : found->second; +} + +void testRepeatedTypesShareOneId() { + SpirvModule module; + const auto first = module.Type(spv::OpTypeInt, 32u, 0u); + const auto second = module.Type(spv::OpTypeInt, 32u, 0u); + check(first == second, "a repeated OpTypeInt 32 0 returned a different id"); + check(declared(module, spv::OpTypeInt) == 1, "a repeated OpTypeInt 32 0 was declared more than once"); + check(module.Type(spv::OpTypeVoid) == module.Type(spv::OpTypeVoid), "a repeated OpTypeVoid returned a different id"); + check(declared(module, spv::OpTypeVoid) == 1, "OpTypeVoid was not declared exactly once"); + check(module.Type(spv::OpTypeBool) == module.Type(spv::OpTypeBool), "a repeated OpTypeBool returned a different id"); + check(module.Type(spv::OpTypeFloat, 32u) == module.Type(spv::OpTypeFloat, 32u), "a repeated OpTypeFloat 32 returned a different id"); + check(declared(module, spv::OpTypeFloat) == 1, "OpTypeFloat was not declared exactly once"); +} + +void testOperandShapesDoNotCollide() { + SpirvModule module; + const auto oneWord = module.Type(spv::OpTypeInt, 32u); + const auto twoWords = module.Type(spv::OpTypeInt, 32u, 0u); + check(oneWord != twoWords, "OpTypeInt with one operand collided with OpTypeInt with two"); + check(declared(module, spv::OpTypeInt) == 2, "the two OpTypeInt shapes were not both declared"); + const auto voidType = module.Type(spv::OpTypeVoid); + const auto boolType = module.Type(spv::OpTypeBool); + check(voidType != boolType, "OpTypeVoid collided with OpTypeBool"); + check(module.Type(spv::OpTypeInt, 32u, 0u) != module.Type(spv::OpTypeInt, 32u, 1u), "unsigned and signed OpTypeInt 32 collided"); + check(module.Type(spv::OpTypeInt, 32u, 0u) != module.Type(spv::OpTypeInt, 64u, 0u), "OpTypeInt 32 collided with OpTypeInt 64"); + check(module.Type(spv::OpTypeFloat, 32u) != module.Type(spv::OpTypeFloat, 64u), "OpTypeFloat 32 collided with OpTypeFloat 64"); +} + +void testOperandKindsAgree() { + SpirvModule module; + const auto fromUnsigned = module.Type(spv::OpTypeInt, 32u, 0u); + const auto fromSigned = module.Type(spv::OpTypeInt, static_cast(32), static_cast(0)); + check(fromUnsigned == fromSigned, "OpTypeInt disagreed between unsigned and signed operands"); + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto fromEnum = module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32); + const auto fromWord = module.Type(spv::OpTypePointer, static_cast(spv::StorageClassFunction), typeU32); + check(fromEnum == fromWord, "OpTypePointer disagreed between enum and integer operands"); + check(fromEnum == module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32), "a repeated enum-operand OpTypePointer returned a different id"); + check(declared(module, spv::OpTypePointer) == 1, "OpTypePointer was not declared exactly once"); +} + +void testDecoratedTypesStayDistinct() { + SpirvModule module; + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto plain = module.Type(spv::OpTypeRuntimeArray, typeU32); + const auto decorated = module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, typeU32); + check(plain != decorated, "a plain OpTypeRuntimeArray collided with the decorated one"); + check(decorated == module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, typeU32), "a repeated decorated type returned a different id"); + check(declared(module, spv::OpTypeRuntimeArray) == 2, "the plain and decorated OpTypeRuntimeArray were not both declared"); +} + +void testConstantsShareOneId() { + SpirvModule module; + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto typeBool = module.Type(spv::OpTypeBool); + check(module.Constant(spv::OpConstant, typeU32, 0u) == module.Constant(spv::OpConstant, typeU32, 0u), "a repeated OpConstant 0 returned a different id"); + check(module.Constant(spv::OpConstant, typeU32, 0u) != module.Constant(spv::OpConstant, typeU32, 1u), "OpConstant 0 collided with OpConstant 1"); + check(module.Constant(spv::OpConstant, typeU32, 1000u) != module.Constant(spv::OpConstant, typeU32, 0u), "OpConstant 1000 collided with OpConstant 0"); + check(module.Constant(spv::OpConstantTrue, typeBool) == module.Constant(spv::OpConstantTrue, typeBool), "a repeated OpConstantTrue returned a different id"); + check(module.Constant(spv::OpConstantTrue, typeBool) != module.Constant(spv::OpConstantFalse, typeBool), "OpConstantTrue collided with OpConstantFalse"); + check(declared(module, spv::OpConstant) == 3, "OpConstant was not declared once per distinct value"); + check(declared(module, spv::OpConstantTrue) == 1, "OpConstantTrue was not declared exactly once"); + const auto signedId = module.Constant(spv::OpConstant, module.Type(spv::OpTypeInt, 32u, 1u), static_cast(-1)); + check(signedId != 0, "a signed OpConstant produced a zero id"); +} + +void testOperandFreeConstantsShareOneId() { + SpirvModule module; + const auto typeBool = module.Type(spv::OpTypeBool); + const auto typeVoid = module.Type(spv::OpTypeVoid); + check(module.Constant(spv::OpConstantTrue, typeBool) == module.Constant(spv::OpConstantTrue, typeBool), "a repeated OpConstantTrue returned a different id"); + check(module.Constant(spv::OpConstantTrue, typeBool) != module.Constant(spv::OpConstantFalse, typeBool), "OpConstantTrue collided with OpConstantFalse"); + check(module.Constant(spv::OpConstantTrue, typeVoid) != module.Constant(spv::OpConstantTrue, typeBool), "OpConstantTrue of different types collided"); + check(declared(module, spv::OpConstantTrue) == 2, "OpConstantTrue was not declared once per type"); + check(declared(module, spv::OpConstantFalse) == 1, "OpConstantFalse was not declared exactly once"); + check(module.Type(spv::OpTypeVoid) == typeVoid, "a repeated operand-free type returned a different id"); +} + +void testIdsSurviveInterleavedAllocation() { + SpirvModule module; + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto constant = module.Constant(spv::OpConstant, typeU32, 4u); + for (int i = 0; i < 8; ++i) { + check(module.AllocateId() != 0, "AllocateId returned zero"); + check(module.Type(spv::OpTypeInt, 32u, 0u) == typeU32, "a memoized type changed id after AllocateId"); + check(module.Constant(spv::OpConstant, typeU32, 4u) == constant, "a memoized constant changed id after AllocateId"); + } +} + +void testModulesAreIndependent() { + SpirvModule first; + SpirvModule second; + const auto firstId = first.Type(spv::OpTypeInt, 32u, 0u); + const auto secondId = second.Type(spv::OpTypeInt, 32u, 0u); + check(firstId == 1, "the first id of a fresh module was not 1"); + check(secondId == 1, "a second module did not start its ids from 1"); + first.Type(spv::OpTypeVoid); + check(second.Type(spv::OpTypeInt, 32u, 0u) == secondId, "a second module's ids moved when the first module was used"); +} + +void testIdsStayUniqueBeyondCacheCapacity() { + SpirvModule module; + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + std::vector ids; + ids.reserve(4096); + for (std::uint32_t value = 0; value < 4096; ++value) { + ids.push_back(module.Constant(spv::OpConstant, typeU32, value)); + } + const std::set distinct(ids.begin(), ids.end()); + check(distinct.size() == ids.size(), "distinct constants shared an id once the cache was overrun"); + for (std::uint32_t value = 0; value < 4096; ++value) { + if (module.Constant(spv::OpConstant, typeU32, value) != ids[value]) { + std::fprintf(stderr, "constant %u changed id after the cache was overrun\n", value); + ++failures; + break; + } + } + std::uint32_t largest = 0; + for (const auto id : distinct) { + largest = largest < id ? id : largest; + } + const auto first = ids.front(); + check(largest - first + 1 == distinct.size(), "the constant ids were not contiguous, so some were never declared"); + check(first > typeU32, "a constant was numbered before the type it uses"); +} + +void testEmittedWordsAreStable() { + SpirvModule module; + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + for (int i = 0; i < 32; ++i) { + module.Type(spv::OpTypeVector, typeU32, 4u); + module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32); + module.Constant(spv::OpConstant, typeU32, static_cast(i % 8)); + module.AddFunction(spv::OpNop); + } + const auto words = module.Finalize(); + check(!words.empty(), "Finalize produced no words"); + const auto counts = opcodeCounts(words); + check(counts.at(spv::OpTypeVector) == 1, "OpTypeVector was declared more than once"); + check(counts.at(spv::OpTypePointer) == 1, "OpTypePointer was declared more than once"); + check(counts.at(spv::OpConstant) == 8, "OpConstant was not declared once per distinct value"); + check(counts.at(spv::OpNop) == 32, "the function body lost instructions"); +} + +} + +int main() { + testRepeatedTypesShareOneId(); + testOperandShapesDoNotCollide(); + testOperandKindsAgree(); + testDecoratedTypesStayDistinct(); + testConstantsShareOneId(); + testOperandFreeConstantsShareOneId(); + testIdsSurviveInterleavedAllocation(); + testModulesAreIndependent(); + testIdsStayUniqueBeyondCacheCapacity(); + testEmittedWordsAreStable(); + if (failures != 0) { + std::fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + return 0; +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index a8e1f5048..6dd990266 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -88,6 +88,16 @@ public: return declareConstant(opcode, std::move(key)); } + std::uint32_t Constant(std::uint32_t opcode, std::uint32_t type) { + const std::uint32_t key[2] = {opcode, type}; + return interned(key, 2u, [this, opcode, type]() { + std::vector keyWords; + keyWords.reserve(2u); + appendOperands(keyWords, opcode, type); + return declareConstant(opcode, std::move(keyWords)); + }); + } + template requires wordSized std::uint32_t Constant(std::uint32_t opcode, std::uint32_t type, TValue value) { From 753e1b934723ad11028a328eed903595bf6d1968 Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:58:23 +0300 Subject: [PATCH 004/203] fix(shader): declare the word-size constraint before the overloads that use it --- .../include/SpirvBackend/SpirvModule.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index 6dd990266..430a6bd93 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -27,6 +27,15 @@ struct SpirvDeferredPhi { }; class SpirvModule { +private: + template + static constexpr bool wordSized = (std::is_integral_v || std::is_enum_v) && sizeof(TOperand) <= sizeof(std::uint32_t); + + template + static std::uint32_t narrowWord(TOperand operand) { + return static_cast(operand); + } + public: explicit SpirvModule(std::uint32_t version = 0x00010300u); [[nodiscard]] std::uint32_t AllocateId(); @@ -130,14 +139,6 @@ private: static constexpr std::size_t DeclarationCacheSize = 512; static constexpr std::size_t MaxCachedDeclarationWords = 4; - template - static constexpr bool wordSized = (std::is_integral_v || std::is_enum_v) && sizeof(TOperand) <= sizeof(std::uint32_t); - - template - static std::uint32_t narrowWord(TOperand operand) { - return static_cast(operand); - } - struct DeclarationSlot { std::array words = {}; std::uint32_t length = 0; From e31f3edf4a1929493a3c4f618a5cefc444ec2be8 Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:59:04 +0300 Subject: [PATCH 005/203] test(shader): register agc_spirv_module_tests with ctest --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ce3801074..8d7731d2e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -503,7 +503,7 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) endforeach() if(BUILD_TESTING) - foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_dpp_write_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests}) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) From cb1f456ce4a4a29a897a3f4214b46c60db812fd2 Mon Sep 17 00:00:00 2001 From: DotDebian Date: Sun, 4 Oct 2026 16:43:54 +0200 Subject: [PATCH 006/203] fix(shader): address ds_append and ds_consume on LDS by the instruction offset alone On LDS the counter is at the instruction offset. The emitter added M0[31:16] to it and required the offset to be below M0[15:0], which is the GDS base and a size no other LDS access reads. The GDS path is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/DsAppendConsume.cpp | 124 ++++++++++++++++++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 22 ++-- 2 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp new file mode 100644 index 000000000..46ad1d7b6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp @@ -0,0 +1,124 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t ActiveLanes = 16; +constexpr std::uint32_t LdsDwords = 256; +constexpr std::uint32_t AppendStart = 1000; +constexpr std::uint32_t ConsumeStart = 2000; +constexpr std::uint32_t MaskedAppendStart = 3000; +constexpr std::uint32_t MaskedConsumeStart = 4000; +constexpr std::uint32_t BaseShiftedStart = 5000; +constexpr std::uint32_t UntouchedAppend = 0x5eed0001u; +constexpr std::uint32_t UntouchedConsume = 0x5eed0002u; +alignas(256) std::array Output{}; + +alignas(256) constexpr auto Code = std::to_array({ + 0x34060084, 0x7e100280, + 0x7e0802ff, AppendStart, 0x7e0a02ff, ConsumeStart, 0x7e0c02ff, MaskedAppendStart, 0x7e0e02ff, MaskedConsumeStart, + 0x7e1c02ff, BaseShiftedStart, 0x7e1e02ff, BaseShiftedStart + 1u, 0x7e2002ff, BaseShiftedStart + 2u, 0x7e2202ff, BaseShiftedStart + 3u, + 0x7e1802ff, UntouchedAppend, 0x7e1a02ff, UntouchedConsume, + 0x7da40080, + 0xd8340010, 0x00000408, 0xd8340014, 0x00000508, 0xd8340018, 0x00000608, 0xd834001c, 0x00000708, + 0xd8340050, 0x00000e08, 0xd8340054, 0x00000f08, 0xd8340058, 0x00001008, 0xd834005c, 0x00001108, + 0xbefe03c1, + 0xbf8cc07f, + 0xbefc03ff, 0x0040ffff, + 0xd8f80010, 0x0a000000, + 0xd8f40014, 0x0b000000, + 0xbefc03ff, 0x00400000, + 0x7da80090, + 0xd8f80018, 0x0c000000, + 0xd8f4001c, 0x0d000000, + 0xbefe03c1, + 0xbf8cc07f, + 0xd8d80010, 0x14000008, 0xd8d80014, 0x15000008, 0xd8d80018, 0x16000008, 0xd8d8001c, 0x17000008, + 0xd8d80050, 0x18000008, 0xd8d80054, 0x19000008, 0xd8d80058, 0x1a000008, 0xd8d8005c, 0x1b000008, + 0xbf8cc07f, + 0xe0702000, 0x80000a03, 0xe0702004, 0x80000b03, 0xe0702008, 0x80000c03, 0xe070200c, 0x80000d03, + 0xe0702010, 0x80001403, 0xe0702014, 0x80001503, 0xe0702018, 0x80001603, 0xe070201c, 0x80001703, + 0xe0702020, 0x80001803, 0xe0702024, 0x80001903, 0xe0702028, 0x80001a03, 0xe070202c, 0x80001b03, + 0xbf810000, +}); + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x01016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + Output.fill(0xdeadbeefu); + std::vector userData(4, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(output.begin(), output.end(), userData.begin()); + const std::span code(Code); + 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}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + constexpr std::array names{ + "ds_append result", "ds_consume result", "masked ds_append result", "masked ds_consume result", + "ds_append counter", "ds_consume counter", "masked ds_append counter", "masked ds_consume counter", + "dword at M0.base + ds_append offset", "dword at M0.base + ds_consume offset", + "dword at M0.base + masked ds_append offset", "dword at M0.base + masked ds_consume offset", + }; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const bool active = tid < ActiveLanes; + const std::array expected{ + AppendStart, ConsumeStart, active ? MaskedAppendStart : UntouchedAppend, active ? MaskedConsumeStart : UntouchedConsume, + AppendStart + Threads, ConsumeStart - Threads, MaskedAppendStart + ActiveLanes, MaskedConsumeStart - ActiveLanes, + BaseShiftedStart, BaseShiftedStart + 1u, BaseShiftedStart + 2u, BaseShiftedStart + 3u, + }; + for (std::uint32_t j = 0; j < expected.size(); ++j) { + const auto actual = Output[tid * Results + j]; + Require(actual == expected[j], std::string("ds append consume: lane ") + std::to_string(tid) + " " + names[j] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[j])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < Threads) { + std::printf("skipped, the device's subgroups are narrower than a wave (%u lanes)\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Run(*device); + Check(); + std::puts("ds append consume tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 4e2fc7c04..a19547554 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -981,11 +981,17 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo } const auto& mem = SharedMemory(ctx, inst); const bool wave64 = state.laneCount == 2u; - const auto m0 = ctx.Arg(inst, 0); - const auto base = Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16u)); - const auto size = Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu)); - const auto address = Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, mem.offset)); - const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + const bool gds = mem.kind == ResourceKind::Gds; + std::uint32_t rawIndex = ConstantU32(state, mem.offset >> 2u); + std::uint32_t m0Bounds = 0; + if (gds) { + const auto m0 = ctx.Arg(inst, 0); + const auto base = Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16u)); + const auto size = Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu)); + const auto address = Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, mem.offset)); + rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + m0Bounds = Binary(state, spv::OpINotEqual, TypeBool(state), size, ConstantU32(state, 0u)); + } const auto access = PrepareMemoryResourceAccess(state, mem); const auto index = EmitMemoryElementIndex(state, access, rawIndex); const auto exec = ctx.Arg(inst, 1); @@ -999,12 +1005,12 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto sourceLane = wave64 ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31u)) : first; const auto isFirst = Binary(state, spv::OpIEqual, TypeBool(state), EmitSubgroupLocalInvocationId(state), sourceLane); const auto storageBounds = EmitMemoryElementInBounds(state, access, index); - const auto m0Bounds = mem.kind == ResourceKind::Gds ? Binary(state, spv::OpINotEqual, TypeBool(state), size, ConstantU32(state, 0u)) : Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mem.offset + 3u), size); + const auto bounds = gds ? AndCondition(state, storageBounds, m0Bounds) : storageBounds; const auto lanesActive = wave64 ? Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0u)) : exec; - const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, AndCondition(state, storageBounds, m0Bounds))); + const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, bounds)); const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() { const auto value = state.module.AllocateId(); - state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, mem.kind == ResourceKind::Gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); + state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); return value; }); const auto result = state.module.AllocateId(); From f08e691b22d9217b56ec81ce42fd6818ee3d9b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 4 Oct 2026 17:09:47 +0200 Subject: [PATCH 007/203] fix(shader): saturate v_cvt_pk_u16_u32/i16_i32; test VOP3 integer ops Co-Authored-By: Claude --- .../tests/execution/Vop3IntegerAlu.cpp | 181 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../Translation/src/IntegerInstructions.cpp | 13 ++ .../Translation/src/VectorInstructions.cpp | 3 +- 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp new file mode 100644 index 000000000..38c609632 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp @@ -0,0 +1,181 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 64; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060088, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd76d000a, 0x041a0b04, 0xd747000b, + 0x041a0b04, 0xd771000c, 0x041a0b04, 0xd772000d, 0x041a0b04, 0xd578000e, 0x041a0b04, 0xd745000f, + 0x041a0b04, 0xd7460010, 0x041a0b04, 0xd76f0011, 0x041a0b04, 0xd5480012, 0x041a0b04, 0xd5490013, + 0x041a0b04, 0xd54a0014, 0x041a0b04, 0xd7630015, 0x02020b04, 0xd7440016, 0x041a0b04, 0xd54e0017, + 0x041a0b04, 0xd54f0018, 0x041a0b04, 0xd5690019, 0x02020b04, 0xd56a001a, 0x02020b04, 0xd56c001b, + 0x02020b04, 0xd542001c, 0x041a0b04, 0xd764001d, 0x02020b04, 0xd6ff001e, 0x02020b04, 0xd7000020, + 0x02020b04, 0xd7010022, 0x02020b04, 0xd5520024, 0x041a0b04, 0xd5550025, 0x041a0b04, 0xd5580026, + 0x041a0b04, 0xd5530027, 0x041a0b04, 0xd5560028, 0x041a0b04, 0xd5590029, 0x041a0b04, 0xd76a002a, + 0x02020b04, 0xd76b002b, 0x02020b04, 0x7e580307, 0xd703002c, 0x02020b04, 0x7e5a0307, 0xd704002d, + 0x02020b04, 0x7e5c0307, 0xd70d002e, 0x02020b04, 0x7e5e0307, 0xd70e002f, 0x02020b04, 0x7e600307, + 0xd7140030, 0x02020b04, 0x7e620307, 0xd7070031, 0x02020b04, 0x7e640307, 0xd7080032, 0x02020b04, + 0x7e660307, 0xd70b0033, 0x02020b04, 0x7e680307, 0xd7090034, 0x02020b04, 0x7e6a0307, 0xd70c0035, + 0x02020b04, 0x7e6c0307, 0xd70a0036, 0x02020b04, 0xd70f0837, 0x02020b04, 0xd5010038, 0x00210280, + 0xd7100839, 0x02020b04, 0xd501003a, 0x00210280, 0xd719083b, 0x02020b04, 0xd501003c, 0x00210280, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, + 0xe0781040, 0x80011a03, 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, + 0xe0781080, 0x80012a03, 0xe0781090, 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, + 0xe07810c0, 0x80013a03, 0xbf810000, +}; + +constexpr std::uint32_t Rows[32][4] = { + {0xf0000000u, 0x0000001cu, 0x00000008u, 0x12345678u}, + {0x80000000u, 0x0000001fu, 0x00000002u, 0xdeadbeefu}, + {0xc0000000u, 0x0000001eu, 0x0000001fu, 0x00000000u}, + {0xabcdef01u, 0x00000000u, 0x00000000u, 0xffffffffu}, + {0x8000ffffu, 0x00000010u, 0x0000001fu, 0xffff0000u}, + {0x7fffffffu, 0x00000001u, 0x0000001fu, 0x55555555u}, + {0x01020304u, 0x05060708u, 0x0c0b0a09u, 0x00000000u}, + {0x80ff7f01u, 0x81827f80u, 0x03020100u, 0x00000001u}, + {0xffff8000u, 0xffff7fffu, 0x08090a0bu, 0xaaaa5555u}, + {0x0000ffffu, 0x00010000u, 0x0d0e0f10u, 0x00000002u}, + {0x00008000u, 0xffffffffu, 0xffff0000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x0000ffffu, 0x25c717e5u}, + {0x00008000u, 0xffff8000u, 0x0000001fu, 0x00000020u}, + {0xdce177fdu, 0xffffffffu, 0x80000000u, 0x000001ffu}, + {0x7fffffffu, 0x00000000u, 0x80000001u, 0x0000001fu}, + {0x80008000u, 0x7fff7fffu, 0xffffffffu, 0x00000040u}, + {0x00000010u, 0xfffffffeu, 0x00000000u, 0x80000001u}, + {0x00010000u, 0xffff0000u, 0x9c015d97u, 0x00000018u}, + {0x00000040u, 0x00000020u, 0x00008000u, 0x4b64789cu}, + {0x00008001u, 0xc7fde805u, 0x040e050fu, 0x309d6b79u}, + {0x0000003fu, 0xffff8000u, 0x07060103u, 0xa66b0d38u}, + {0x00ffffffu, 0x7fff7fffu, 0x84e55160u, 0x7fffffffu}, + {0x75139237u, 0x6822a6b2u, 0xfee5a5b2u, 0x154cd2aau}, + {0x0000003fu, 0x00000001u, 0x0000000fu, 0x7fffffffu}, + {0xd8e94b15u, 0x36a80bdfu, 0x00000003u, 0xffff8000u}, + {0x00000020u, 0x45100358u, 0x80000001u, 0xffff7fffu}, + {0x00000040u, 0x02cdf2afu, 0x0c0c0406u, 0x00008001u}, + {0x00000021u, 0x7fff7fffu, 0x00000001u, 0xffff8000u}, + {0x93b3a3d9u, 0x00000017u, 0xffffffffu, 0x0000001fu}, + {0x00000007u, 0x00000021u, 0x00000040u, 0x00007fffu}, + {0x7fff7fffu, 0x80008000u, 0x00010000u, 0x00000017u}, + {0x0000001fu, 0x00000020u, 0x0000003fu, 0x3253b562u} +}; +constexpr std::uint32_t Expected[32][51] = { + {0xf0000024u, 0x00001c00u, 0x00000008u, 0xf000001cu, 0xf0000014u, 0xf0000024u, 0x00000008u, 0x00000008u, 0x0000000fu, 0xffffffffu, 0x00000008u, 0x00000000u, 0x1c1c1c00u, 0x00000000u, 0x0000001cu, 0x40000000u, 0x0000001au, 0xfffffffeu, 0x00000008u, 0x00000020u, 0x0000001cu, 0x00000008u, 0x0000001cu, 0x00000008u, 0x0000001cu, 0x00000008u, 0xf0000000u, 0x0000001cu, 0x00000008u, 0x00000008u, 0xf0000000u, 0x0000001cu, 0x001cffffu, 0x001c8000u, 0x1234001cu, 0x1234ffe4u, 0x1234001cu, 0x1234ffe4u, 0x1234001cu, 0x1234001cu, 0x1234001cu, 0x12340000u, 0x1234001cu, 0x12340000u, 0x1234001cu, 0xf000001cu, 0x00000000u, 0xefffffe4u, 0x00000000u, 0x1000001cu, 0x00000001u}, + {0x80000021u, 0x0000007cu, 0x00000002u, 0x8000001fu, 0x8000001du, 0x80000021u, 0x00000002u, 0x00000002u, 0x00000001u, 0xffffffffu, 0x00000002u, 0x00000000u, 0x1f1f1f00u, 0x00000007u, 0x00000000u, 0x80000000u, 0x0000000fu, 0xfffffff0u, 0x00000002u, 0x00000020u, 0x0000001fu, 0x00000002u, 0x0000001fu, 0x00000002u, 0x0000001fu, 0x00000002u, 0x80000000u, 0x0000001fu, 0x00000002u, 0x00000002u, 0x80000000u, 0x0000001fu, 0x001fffffu, 0x001f8000u, 0xdead001fu, 0xdeadffe1u, 0xdead001fu, 0xdeadffe1u, 0xdead001fu, 0xdead001fu, 0xdead001fu, 0xdead0000u, 0xdead001fu, 0xdead0000u, 0xdead001fu, 0x8000001fu, 0x00000000u, 0x7fffffe1u, 0x00000000u, 0x8000001fu, 0x00000001u}, + {0xc000003du, 0x00000000u, 0x0000001fu, 0xc000001fu, 0xc0000001u, 0xc000003du, 0x0000001fu, 0x0000001fu, 0x00000003u, 0xffffffffu, 0x0000001fu, 0x00000000u, 0x1e1e1effu, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000016u, 0xfffffff8u, 0x0000001fu, 0x00000020u, 0x0000001eu, 0x0000001fu, 0x0000001eu, 0x0000001fu, 0x0000001eu, 0x0000001fu, 0xc0000000u, 0x0000001fu, 0x0000001eu, 0x0000001eu, 0xc0000000u, 0x0000001fu, 0x001effffu, 0x001e8000u, 0x0000001eu, 0x0000ffe2u, 0x0000001eu, 0x0000ffe2u, 0x0000001eu, 0x0000001eu, 0x0000001eu, 0x00000000u, 0x0000001eu, 0x00000000u, 0x0000001eu, 0xc000001eu, 0x00000000u, 0xbfffffe2u, 0x00000000u, 0x4000001eu, 0x00000001u}, + {0xabcdef01u, 0xabcdef01u, 0x00000000u, 0xabcdef01u, 0xabcdef01u, 0xabcdef01u, 0xabcdef01u, 0xabcdef01u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000012u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xabcdef01u, 0x00000000u, 0x00000000u, 0x00000000u, 0xabcdef01u, 0x00000000u, 0x0000ffffu, 0x00008000u, 0xffffef01u, 0xffffef01u, 0xffffef01u, 0xffffef01u, 0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffffef01u, 0xffffef01u, 0xffff0000u, 0xabcdef01u, 0x00000000u, 0xabcdef01u, 0x00000000u, 0x543210ffu, 0x00000001u}, + {0x8001002eu, 0x80000000u, 0x0000001fu, 0x8000ffffu, 0x8000fff0u, 0x8001000eu, 0xffff001fu, 0xffff001fu, 0x00008000u, 0xffff8000u, 0x00000010u, 0xffff0000u, 0x101010ffu, 0x0001fffeu, 0x00ffff00u, 0x000ffff0u, 0x00000008u, 0xfffffff8u, 0x0010000fu, 0x00000021u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x8000ffffu, 0x0000001fu, 0x00000010u, 0x00000010u, 0x8000ffffu, 0x0000001fu, 0x0010ffffu, 0x00108000u, 0xffff000fu, 0xffffffefu, 0xffff000fu, 0xffffffefu, 0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0010u, 0xffffffffu, 0xffffffffu, 0xffff0010u, 0x8001000fu, 0x00000000u, 0x8000ffefu, 0x00000000u, 0x7fff0011u, 0x00000001u}, + {0x8000001fu, 0x00000000u, 0x0000001fu, 0x7fffffffu, 0x7fffffe1u, 0x8000001du, 0x0000001du, 0xffffffffu, 0x3fffffffu, 0x3fffffffu, 0x00000001u, 0xfffffffeu, 0x010101ffu, 0xfffffffeu, 0xffffff00u, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x0000001eu, 0x00000020u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7fffffffu, 0x0000001fu, 0x00000001u, 0x7fffffffu, 0x0000001fu, 0x0001ffffu, 0x00017fffu, 0x55550000u, 0x5555fffeu, 0x55550000u, 0x5555fffeu, 0x55558000u, 0x55550000u, 0x55550000u, 0x55550001u, 0x5555ffffu, 0x5555ffffu, 0x55550001u, 0x80000000u, 0x00000000u, 0x7ffffffeu, 0x00000000u, 0x80000002u, 0x00000001u}, + {0x12131415u, 0x10141800u, 0x0d0b0b09u, 0x0d0f0f0du, 0x080f0e05u, 0x100f0e15u, 0x0e0e0e09u, 0x0e0b0e09u, 0x00000003u, 0x00000003u, 0x0d0b0b09u, 0x00000f00u, 0x00000000u, 0x82028303u, 0x04050607u, 0x3c3d3420u, 0x00051022u, 0x00051022u, 0x2c483e29u, 0x0506070du, 0x50607080u, 0xc0b0a090u, 0x90506070u, 0x00c0b0a0u, 0x90506070u, 0x00c0b0a0u, 0x01020304u, 0x0c0b0a09u, 0x05060708u, 0x01020304u, 0x0c0b0a09u, 0x05060708u, 0xffffffffu, 0x7fff7fffu, 0x00000a0cu, 0x0000fbfcu, 0x00000a0cu, 0x0000fbfcu, 0x00007080u, 0x00000070u, 0x00000070u, 0x00000304u, 0x00000708u, 0x00000304u, 0x00000708u, 0x06080a0cu, 0x00000000u, 0xfbfbfbfcu, 0x00000001u, 0x04040404u, 0x00000000u}, + {0x0583ff81u, 0x0281fe81u, 0x83827f00u, 0x83ff7f81u, 0x027f0181u, 0x047f0181u, 0x84018001u, 0x83ff7f01u, 0x00000000u, 0x00000000u, 0x83827f00u, 0x00000001u, 0x81827f80u, 0x81827f80u, 0x81827f80u, 0x3f42ff80u, 0x414280fdu, 0x3ec0827cu, 0x40450080u, 0x81827f91u, 0x0304ff00u, 0x06040201u, 0x40c13fc0u, 0x01810080u, 0x40c13fc0u, 0x01810080u, 0x80ff7f01u, 0x03020100u, 0x81827f80u, 0x03020100u, 0x81827f80u, 0x80ff7f01u, 0xffffffffu, 0x80008000u, 0x0000fe81u, 0x0000ff81u, 0x0000fe81u, 0x0000ff81u, 0x0000ff00u, 0x00003fc0u, 0x00003fc0u, 0x00007f01u, 0x00007f80u, 0x00007f01u, 0x00007f80u, 0x0281fe81u, 0x00000001u, 0xff7cff81u, 0x00000001u, 0x0083007fu, 0x00000000u}, + {0x08080a0au, 0xf7fff800u, 0xffff0a0bu, 0xffffffffu, 0x0809f5f4u, 0x080a0a0au, 0x08090a0bu, 0x08090a0bu, 0x00000001u, 0xffffffffu, 0xffff0a0bu, 0x00000000u, 0x00ffffffu, 0x001fffefu, 0xff8000ffu, 0x40008000u, 0xfffeffffu, 0x00000000u, 0x48098a0bu, 0xffff8010u, 0xffff7fffu, 0x08090a0bu, 0xffff7fffu, 0x08090a0bu, 0xffff7fffu, 0x08090a0bu, 0xffff7fffu, 0x08090a0bu, 0xffff8000u, 0x08090a0bu, 0xffff8000u, 0xffff7fffu, 0xffffffffu, 0x80008000u, 0xaaaaffffu, 0xaaaa0001u, 0xaaaaffffu, 0xaaaa0001u, 0xaaaa7fffu, 0xaaaa7fffu, 0xaaaa7fffu, 0xaaaa7fffu, 0xaaaa8000u, 0xaaaa8000u, 0xaaaa7fffu, 0xfffeffffu, 0x00000001u, 0x00000001u, 0x00000000u, 0xffffffffu, 0x00000001u}, + {0x0d100f0fu, 0xffff0000u, 0x0d0e0f10u, 0x0d0fffffu, 0x0d0ff0efu, 0x0d100f0fu, 0x0d0f0f0fu, 0x0d0effffu, 0x0000ffffu, 0xffffffffu, 0x0d0e0000u, 0x7fffffffu, 0xffffffffu, 0xffff0001u, 0x00010000u, 0xffff0000u, 0x00000000u, 0x00000000u, 0x0d0d0f10u, 0x00010010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x0d0e0f10u, 0x00010000u, 0x0000ffffu, 0x0d0e0f10u, 0x00010000u, 0xffffffffu, 0x7fff7fffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x0001ffffu, 0x00000000u, 0xffffffffu, 0x00000001u, 0x00000001u, 0x00000000u}, + {0xffff7fffu, 0x00007fffu, 0xffff8000u, 0xffffffffu, 0x00007fffu, 0xfffe7fffu, 0xffff0000u, 0xffff0000u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffff8000u, 0x00007fffu, 0xffffffffu, 0xfffe8000u, 0x00000000u, 0xffffffffu, 0xffff0000u, 0xffffffffu, 0xffff0000u, 0xffffffffu, 0xffff0000u, 0xffff0000u, 0x00008000u, 0xffffffffu, 0x00008000u, 0xffffffffu, 0xffff0000u, 0xffff8000u, 0xffff7fffu, 0x00007fffu, 0x00008001u, 0x00007fffu, 0x00008001u, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00008000u, 0x0000ffffu, 0x00008000u, 0x0000ffffu, 0x00007fffu, 0x00000001u, 0x00008001u, 0x00000001u, 0xffff7fffu, 0x00000000u}, + {0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x0000001fu, 0x00000000u, 0x0000801fu, 0xffff801fu, 0xffff001fu, 0xffff001fu, 0x0000801fu, 0x0000801fu, 0x00008000u, 0x00008000u, 0x0000801fu, 0x00000000u, 0x000000ffu, 0x00010001u, 0x008000ffu, 0xc0000000u, 0x00007fffu, 0xffffffffu, 0xc000001fu, 0xffff8001u, 0xffff8000u, 0x0000001fu, 0xffff8000u, 0x0000001fu, 0xffff8000u, 0x0000001fu, 0xffff8000u, 0x00008000u, 0x0000001fu, 0x0000001fu, 0xffff8000u, 0x00008000u, 0xffff8000u, 0x80007fffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00000001u, 0x00010000u, 0x00000001u, 0xffff0000u, 0x00000000u}, + {0x5ce177fcu, 0xdce177fcu, 0xdce177fdu, 0xffffffffu, 0xa31e8802u, 0xa31e8802u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xdce177fdu, 0x80000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x231e8803u, 0xdce177fcu, 0x00000000u, 0x801e8803u, 0x00000015u, 0x00000000u, 0xe0000000u, 0x00000004u, 0x00000000u, 0xfffffffcu, 0xffffffffu, 0x80000000u, 0xffffffffu, 0xdce177fdu, 0x80000000u, 0xffffffffu, 0xdce177fdu, 0xffffffffu, 0xffff8000u, 0x000077fcu, 0x000077feu, 0x000077fcu, 0x000077feu, 0x0000e000u, 0x00000007u, 0x0000ffffu, 0x000077fdu, 0x0000ffffu, 0x0000ffffu, 0x000077fdu, 0xdce177fcu, 0x00000001u, 0xdce177feu, 0x00000001u, 0x231e8802u, 0x00000000u}, + {0x00000000u, 0xfffffffeu, 0x80000001u, 0xffffffffu, 0xfffffffeu, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000001u, 0xffffffffu, 0x80000000u, 0x7fffffffu, 0xff000000u, 0x80000000u, 0xff000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000001u, 0x0000001fu, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x80000001u, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x80000001u, 0x7fffffffu, 0x0000ffffu, 0x00007fffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x80000001u, 0x00000001u}, + {0xfffffffeu, 0x80000000u, 0xffffffffu, 0xffffffffu, 0x00000000u, 0xfffffffeu, 0xffffffffu, 0xffffffffu, 0x00000001u, 0xffffffffu, 0x7fff7fffu, 0x00000000u, 0xffffffffu, 0x00010000u, 0x0080007fu, 0x3fff8000u, 0x3fffffffu, 0xc0008000u, 0xbfff7fffu, 0x7fff8001u, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0xffffffffu, 0x80008000u, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0xffffffffu, 0x80008000u, 0xffffffffu, 0x7fff8000u, 0x0000ffffu, 0x00000001u, 0x0000ffffu, 0x00000001u, 0x00007fffu, 0x00007fffu, 0x00007fffu, 0x00007fffu, 0x00008000u, 0x00008000u, 0x00007fffu, 0xffffffffu, 0x00000000u, 0x00010001u, 0x00000000u, 0xfffeffffu, 0x00000001u}, + {0x0000000eu, 0x0000000eu, 0x00000010u, 0xfffffffeu, 0xffffffeeu, 0xffffffeeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000010u, 0xc0000000u, 0xfefefefeu, 0xfffffffeu, 0xfffffffeu, 0xffffffe0u, 0x0000000fu, 0xffffffffu, 0xffffffe0u, 0xffffffffu, 0xfffe0000u, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0xfffffffeu, 0x00000010u, 0x00000000u, 0x00000000u, 0xfffffffeu, 0x00000010u, 0xffff0010u, 0xfffe0010u, 0x8000000eu, 0x80000012u, 0x8000000eu, 0x80000012u, 0x8000fffeu, 0x8000fffeu, 0x8000fffeu, 0x80000010u, 0x8000fffeu, 0x8000fffeu, 0x80000010u, 0x0000000eu, 0x00000001u, 0x00000012u, 0x00000001u, 0xffffffeeu, 0x00000000u}, + {0x9c015d97u, 0x00000000u, 0x9c015d97u, 0xffff5d97u, 0x63ff5d97u, 0x9bff5d97u, 0x9c025d97u, 0x9c015d97u, 0x00010000u, 0x00010000u, 0x9c015d97u, 0x00000000u, 0xff00ffffu, 0x020001ffu, 0x010000ffu, 0x00000000u, 0x0000ffffu, 0xffffffffu, 0x9c015d97u, 0xffff0001u, 0xffff0000u, 0x9c015d97u, 0xffff0000u, 0x9c015d97u, 0xffff0000u, 0x9c015d97u, 0x9c015d97u, 0x00010000u, 0xffff0000u, 0x00010000u, 0xffff0000u, 0x9c015d97u, 0xffffffffu, 0x80007fffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00020000u, 0x00000001u, 0xfffe0000u, 0x00000000u}, + {0x00008060u, 0x00000060u, 0x00008000u, 0x00008060u, 0x00008060u, 0x00008060u, 0x00008040u, 0x00008040u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x2020ff20u, 0x00000020u, 0x00000020u, 0x00000800u, 0x00000000u, 0x00000000u, 0x00008800u, 0x00000021u, 0x00000020u, 0x00008000u, 0x00000020u, 0x00008000u, 0x00000020u, 0x00008000u, 0x00000020u, 0x00008000u, 0x00000040u, 0x00000020u, 0x00008000u, 0x00000040u, 0x00200040u, 0x00200040u, 0x4b640060u, 0x4b640020u, 0x4b640060u, 0x4b640020u, 0x4b640020u, 0x4b640020u, 0x4b640020u, 0x4b640020u, 0x4b640040u, 0x4b640020u, 0x4b640040u, 0x00000060u, 0x00000000u, 0x00000020u, 0x00000000u, 0xffffffe0u, 0x00000001u}, + {0xcc0c6d15u, 0x34030000u, 0x040e850fu, 0xc7ffed0fu, 0xc3f36d0bu, 0xcc0b6d13u, 0x041e052fu, 0x041e052fu, 0x00000400u, 0x00000400u, 0x040e850fu, 0x00000020u, 0x01ff80ffu, 0x00038ffbu, 0x008001c7u, 0xbc006805u, 0x000063ffu, 0xffffe3feu, 0xf80e6d14u, 0xc7fde807u, 0x8ffbd00au, 0x081c0a1fu, 0xe3fef402u, 0x02070287u, 0xe3fef402u, 0x02070287u, 0xc7fde805u, 0x040e050fu, 0x00008001u, 0x00008001u, 0xc7fde805u, 0x040e050fu, 0xffff8001u, 0x80007fffu, 0x309d6806u, 0x309d97fcu, 0x309d6806u, 0x309d97fcu, 0x309dd00au, 0x309d7402u, 0x309df402u, 0x309d8001u, 0x309de805u, 0x309d8001u, 0x309de805u, 0xc7fe6806u, 0x00000000u, 0x380297fcu, 0x00000001u, 0xc7fd6804u, 0x00000000u}, + {0x07058142u, 0xfffc01f8u, 0x07060103u, 0xffff813fu, 0xf8f9813cu, 0x07058142u, 0x07060142u, 0x0706013fu, 0x00000007u, 0xffffffffu, 0x07060100u, 0x7fffffffu, 0x000080ffu, 0xfffff000u, 0x00003fffu, 0xffe08000u, 0x0000003eu, 0xffffffffu, 0x06e68103u, 0xffff8006u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x07060103u, 0x0000003fu, 0x0000003fu, 0xffff8000u, 0x07060103u, 0xffff003fu, 0x8000003fu, 0xa66b803fu, 0xa66b803fu, 0xa66b803fu, 0xa66b803fu, 0xa66b0000u, 0xa66b0001u, 0xa66bffffu, 0xa66b003fu, 0xa66b8000u, 0xa66b8000u, 0xa66b003fu, 0xffff803fu, 0x00000000u, 0x0000803fu, 0x00000001u, 0xffff7fc1u, 0x00000000u}, + {0x05e4d15eu, 0x80ff7ffeu, 0x84ff7fffu, 0xffffffffu, 0xfbe5d160u, 0x03e5d160u, 0x04e55160u, 0x84e55160u, 0x00000000u, 0x00000000u, 0x84ff7fffu, 0x80000000u, 0xffffffffu, 0x7fff7fffu, 0x7fff7fffu, 0x7f008001u, 0x007fff7fu, 0x007fff7fu, 0x84e5d161u, 0x7fff8017u, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x84e55160u, 0x7fff7fffu, 0x00ffffffu, 0x00ffffffu, 0x84e55160u, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0x7fff7ffeu, 0x7fff8000u, 0x7fff7ffeu, 0x7fff8000u, 0x7fff8000u, 0x7fff0000u, 0x7fff0000u, 0x7fff7fffu, 0x7fffffffu, 0x7fffffffu, 0x7fff7fffu, 0x80ff7ffeu, 0x00000000u, 0x81008000u, 0x00000001u, 0x7eff8000u, 0x00000000u}, + {0xdc1bde9bu, 0xe3a40000u, 0xfee7a7b2u, 0xfff7b7b7u, 0xe3d49137u, 0x1c16da37u, 0x47c1a5b2u, 0xfefda5b2u, 0x00001d44u, 0x00001d44u, 0xeae6a7b2u, 0xfffc0000u, 0xffffffffu, 0xe48dda08u, 0x92376822u, 0xdbb9543eu, 0x2f9fcc3bu, 0x2f9fcc3bu, 0x289ef9f0u, 0x6822a6c2u, 0x00000000u, 0x59000000u, 0x000001fdu, 0x00000000u, 0xfffffffdu, 0xffffffffu, 0xfee5a5b2u, 0x75139237u, 0x6822a6b2u, 0x6822a6b2u, 0xfee5a5b2u, 0x75139237u, 0xffffffffu, 0x7fff7fffu, 0x154c38e9u, 0x154ceb85u, 0x154c38e9u, 0x154ceb85u, 0x154c5900u, 0x154c014du, 0x154cff4du, 0x154c9237u, 0x154ca6b2u, 0x154c9237u, 0x154ca6b2u, 0xdd3638e9u, 0x00000000u, 0x0cf0eb85u, 0x00000000u, 0xf30f147bu, 0x00000001u}, + {0x0000004fu, 0x00200000u, 0x0000000fu, 0x0000003fu, 0x00000031u, 0x0000004du, 0x0000008du, 0x0000007fu, 0x0000001fu, 0x0000001fu, 0x00000001u, 0xfffffffeu, 0x010101ffu, 0x007e0000u, 0x00003f00u, 0x0000003fu, 0x00000000u, 0x00000000u, 0x0000004eu, 0x00000007u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x0000003fu, 0x0000000fu, 0x00000001u, 0x0000003fu, 0x0000000fu, 0x0001003fu, 0x0001003fu, 0x7fff0040u, 0x7fff003eu, 0x7fff0040u, 0x7fff003eu, 0x7fff8000u, 0x7fff0000u, 0x7fff0000u, 0x7fff0001u, 0x7fff003fu, 0x7fff0001u, 0x7fff003fu, 0x00000040u, 0x00000000u, 0x0000003eu, 0x00000000u, 0xffffffc2u, 0x00000001u}, + {0x0f9156f7u, 0x7c8ab7a0u, 0x10a80b17u, 0xfee94bdfu, 0xee4140c9u, 0xee4140cdu, 0x80000003u, 0x80000003u, 0x00000001u, 0xffffffffu, 0x10a80b17u, 0x80000000u, 0xdfdfdf36u, 0xa6d5017bu, 0xe94b1536u, 0xad3a4e4bu, 0x2e4f98feu, 0xf7a78d1fu, 0x233a4e4eu, 0x36a80befu, 0x7be00000u, 0x0066d501u, 0x000019b5u, 0x00000000u, 0x000019b5u, 0x00000000u, 0xd8e94b15u, 0x36a80bdfu, 0x00000003u, 0x00000003u, 0xd8e94b15u, 0x36a80bdfu, 0xffffffffu, 0x7fff8000u, 0xffff56f4u, 0xffff3f36u, 0xffff56f4u, 0xffff3f36u, 0xffff7be0u, 0xffff005eu, 0xffff005eu, 0xffff0bdfu, 0xffff4b15u, 0xffff0bdfu, 0xffff4b15u, 0x0f9156f4u, 0x00000001u, 0xa2413f36u, 0x00000000u, 0x5dbec0cau, 0x00000001u}, + {0xc5100379u, 0x8a2006f0u, 0x80000001u, 0xc5100379u, 0xc5100379u, 0xc5100379u, 0xa0000001u, 0xa0000001u, 0x00000000u, 0x00000000u, 0x80000001u, 0x00000000u, 0xff585803u, 0x228801acu, 0x20451003u, 0xa2006b00u, 0x00000008u, 0x00000008u, 0x82006b01u, 0x45100359u, 0x00000000u, 0x45100358u, 0x80000001u, 0x00000000u, 0x80000001u, 0xffffffffu, 0x80000001u, 0x45100358u, 0x00000020u, 0x00000020u, 0x80000001u, 0x45100358u, 0xffff0020u, 0x7fff0020u, 0xffff0378u, 0xfffffcc8u, 0xffff0378u, 0xfffffcc8u, 0xffff0358u, 0xffff0358u, 0xffff0358u, 0xffff0020u, 0xffff0358u, 0xffff0020u, 0xffff0358u, 0x45100378u, 0x00000000u, 0xbaeffcc8u, 0x00000001u, 0x45100338u, 0x00000000u}, + {0x0ed9f6f5u, 0xb37cbbc0u, 0x0c0c0406u, 0x0ecdf6efu, 0x0ec1f6e9u, 0x0ed9f6f5u, 0x0c2c0406u, 0x0c2c0406u, 0x00000000u, 0x00000000u, 0x0c0c0406u, 0x00000000u, 0x00004000u, 0x000b37cau, 0x004002cdu, 0xb37cabc0u, 0x00000000u, 0x00000000u, 0xff88afc6u, 0x02cdf2b0u, 0x02cdf2afu, 0x0c0c0406u, 0x02cdf2afu, 0x0c0c0406u, 0x02cdf2afu, 0x0c0c0406u, 0x00000040u, 0x0c0c0406u, 0x02cdf2afu, 0x00000040u, 0x0c0c0406u, 0x02cdf2afu, 0xffff0040u, 0x7fff0040u, 0x0000f2efu, 0x00000d91u, 0x0000f2efu, 0x00000d91u, 0x0000f2afu, 0x0000f2afu, 0x0000f2afu, 0x00000040u, 0x0000f2afu, 0x0000f2afu, 0x00000040u, 0x02cdf2efu, 0x00000000u, 0xfd320d91u, 0x00000001u, 0x02cdf26fu, 0x00000000u}, + {0x7fff8021u, 0xffff0040u, 0x00000021u, 0x7fff7fffu, 0x7fff7fdfu, 0x7fff7fdfu, 0x80000001u, 0x80000001u, 0x00000000u, 0x00000000u, 0x00000021u, 0x80000000u, 0xffffff7fu, 0xbfffbfffu, 0x217fff7fu, 0x7fef7fdfu, 0x00000010u, 0x00000010u, 0xffef7fe0u, 0x7fff8001u, 0x00000000u, 0xfffefffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7fff7fffu, 0x00000021u, 0x00000001u, 0x7fff7fffu, 0x00000021u, 0xffff0021u, 0x7fff0021u, 0xffff8020u, 0xffff8022u, 0xffff8020u, 0xffff8022u, 0xfffffffeu, 0xffff3fffu, 0xffff3fffu, 0xffff0021u, 0xffff7fffu, 0xffff0021u, 0xffff7fffu, 0x7fff8020u, 0x00000000u, 0x80008022u, 0x00000001u, 0x7fff7fdeu, 0x00000000u}, + {0x93b3a3efu, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x6c4c5c31u, 0x93b3a3cdu, 0xec7fffffu, 0xffffffffu, 0x00000127u, 0xffffff27u, 0x6c4c5c37u, 0xff800000u, 0xffffffffu, 0x276747b2u, 0xb3a3d900u, 0x4523b87fu, 0x0000000du, 0xfffffff6u, 0xf923b87eu, 0x00000029u, 0x2e000000u, 0xfe000000u, 0xffffff80u, 0x0000007fu, 0xffffff80u, 0xffffffffu, 0x93b3a3d9u, 0x00000017u, 0xffffffffu, 0x00000017u, 0xffffffffu, 0x93b3a3d9u, 0x0017ffffu, 0x00178000u, 0x0000a3f0u, 0x0000a3c2u, 0x0000a3f0u, 0x0000a3c2u, 0x00002e00u, 0x00000000u, 0x00000000u, 0x00000017u, 0x0000a3d9u, 0x0000a3d9u, 0x00000017u, 0x93b3a3f0u, 0x00000000u, 0x93b3a3c2u, 0x00000000u, 0x6c4c5c3eu, 0x00000001u}, + {0x00000068u, 0x00000028u, 0x00000041u, 0x00000067u, 0x00000066u, 0x00000066u, 0x0000004eu, 0x0000004eu, 0x00000000u, 0x00000000u, 0x00000041u, 0x000000feu, 0x212121ffu, 0x00000021u, 0x00000021u, 0x000000e7u, 0x00000000u, 0x00000000u, 0x00000127u, 0x00000024u, 0x00001080u, 0x00002000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000007u, 0x00000040u, 0x00000021u, 0x00000007u, 0x00000040u, 0x00000021u, 0x00210007u, 0x00210007u, 0x00000028u, 0x0000ffe6u, 0x00000028u, 0x0000ffe6u, 0x00001080u, 0x00000000u, 0x00000000u, 0x00000007u, 0x00000021u, 0x00000007u, 0x00000021u, 0x00000028u, 0x00000000u, 0xffffffe6u, 0x00000001u, 0x0000001au, 0x00000000u}, + {0x0000ffffu, 0xffffffffu, 0x00010000u, 0xffffffffu, 0xfffeffffu, 0x0000ffffu, 0x80007fffu, 0x7fff7fffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fffffffu, 0x00800000u, 0x80008000u, 0x80008000u, 0x3fff8000u, 0x3fffffffu, 0xc0008000u, 0xc0008000u, 0x8000801eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80008000u, 0x7fff7fffu, 0x00010000u, 0x00010000u, 0x80008000u, 0x7fff7fffu, 0xffffffffu, 0x80007fffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000001u, 0x0000ffffu, 0x00007fffu, 0x00008000u, 0x00008000u, 0x00007fffu, 0xffffffffu, 0x00000000u, 0xfffeffffu, 0x00000001u, 0x00010001u, 0x00000000u}, + {0x0000007eu, 0x80000000u, 0x0000003fu, 0x0000003fu, 0x00000000u, 0x0000007eu, 0x0000005eu, 0x0000003fu, 0x0000001fu, 0x0000001fu, 0x00000020u, 0x7fffffffu, 0x202020ffu, 0x0000003eu, 0x00001f00u, 0x000003e0u, 0x00000000u, 0x00000000u, 0x0000041fu, 0x00000025u, 0x00000000u, 0x80000010u, 0x0000007eu, 0x00000000u, 0x0000007eu, 0x00000000u, 0x0000001fu, 0x0000003fu, 0x00000020u, 0x0000001fu, 0x0000003fu, 0x00000020u, 0x0020001fu, 0x0020001fu, 0x3253003fu, 0x3253ffffu, 0x3253003fu, 0x3253ffffu, 0x32530000u, 0x32530000u, 0x32530000u, 0x3253001fu, 0x32530020u, 0x3253001fu, 0x32530020u, 0x0000003fu, 0x00000000u, 0xffffffffu, 0x00000001u, 0x00000001u, 0x00000000u} +}; +constexpr const char* Names[51] = {"add3_u32", "add_lshl_u32", "and_or_b32", "or3_b32", "xor3_b32", "xad_u32", "lshl_add_u32", "lshl_or_b32", "bfe_u32", "bfe_i32", "bfi_b32", "bfm_b32", "perm_b32", "alignbit_b32", "alignbyte_b32", "mul_lo_u32", "mul_hi_u32", "mul_hi_i32", "mad_i32_i24", "bcnt_u32_b32", "lshlrev_b64 lo", "lshlrev_b64 hi", "lshrrev_b64 lo", "lshrrev_b64 hi", "ashrrev_i64 lo", "ashrrev_i64 hi", "min3_i32", "max3_i32", "med3_i32", "min3_u32", "max3_u32", "med3_u32", "cvt_pk_u16_u32", "cvt_pk_i16_i32", "add_nc_u16", "sub_nc_u16", "add_nc_i16", "sub_nc_i16", "lshlrev_b16", "lshrrev_b16", "ashrrev_i16", "min_u16", "max_u16", "min_i16", "max_i16", "add_co_u32", "add_co_u32 carry", "sub_co_u32", "sub_co_u32 carry", "subrev_co_u32", "subrev_co_u32 carry"}; + +void Fill(std::uint32_t tid, std::uint32_t* words) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("vop3 integer alu: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 51; ++i) Expect(tid, out[i], Expected[tid][i], Names[i]); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("vop3 integer alu tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 4be3e2626..95d686744 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -229,6 +229,7 @@ private: bool integerDot(const RdnaInstruction& inst, std::uint32_t elementBits, bool sign, bool accumulator); bool vCndmaskB32(const RdnaInstruction& inst); bool packB16(const RdnaInstruction& inst, bool high0, bool high1); + bool vCvtPk16I32(const RdnaInstruction& inst, bool sign); void sSubvectorLoop(const RdnaInstruction& inst, bool begin); void sSaveexec(const RdnaInstruction& inst, IrOpcode operation, bool negateExec, bool negateSource, bool write64, bool negateResult = false, bool writeDestination = true); void addU32(const RdnaInstruction& inst, bool vector, bool useCarryIn); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index 7d80dca18..9ef0149ac 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -812,6 +812,19 @@ bool TranslationContext::packB16(const RdnaInstruction& inst, bool high0, bool h return true; } +bool TranslationContext::vCvtPk16I32(const RdnaInstruction& inst, bool sign) { + const auto saturate = [&](std::uint32_t index) { + const IrU32 source = readU32(sourceAt(inst, index)); + if (sign) { + return IrU32(ir.Emit(IrOpcode::SMax32, IrType::U32, {&ir.Emit(IrOpcode::SMin32, IrType::U32, {&source.Value(), &ir.Constant(0x7fffu)}), &ir.Constant(0xffff8000u)})); + } + return IrU32(ir.Emit(IrOpcode::UMin32, IrType::U32, {&source.Value(), &ir.Constant(0xffffu)})); + }; + const IrU32 result = packU16Lanes(saturate(0u), saturate(1u)); + writeOperand(inst.destination, &result.Value()); + return true; +} + void TranslateIntegerInstruction(TranslationContext& context, const RdnaInstruction& instruction) { throw std::runtime_error("TranslateIntegerInstruction not implemented"); } diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 2782618ee..93280d9f6 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -720,8 +720,9 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { vPackB32F16(inst); return true; case RdnaOpcode::VCvtPkU16U32: + return vCvtPk16I32(inst, false); case RdnaOpcode::VCvtPkI16I32: - return packB16(inst, false, false); + return vCvtPk16I32(inst, true); case RdnaOpcode::VLshlrevB16: return integer16Shift(inst, IrOpcode::ShiftLeftLogical32, false); case RdnaOpcode::VLshrrevB16: From bc1272269829b6d905f0dde112e68bb2eea5a6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 4 Oct 2026 18:49:49 +0200 Subject: [PATCH 008/203] fix(shader): convert unorm and snorm buffer format loads with correct rounding Format loads divided by 2^b-1 with OpFDiv, which Vulkan only bounds to 2.5 ULP; RADV returns a different f32 than the hardware for many 8- and 16-bit values. The quotient is now built on the bits: v/(2^b-1) is v's b bits repeated, so the mantissa is the rotated value replicated and rounded on the next bit (no ties). Adds an execution test of buffer_load_format_x..xyzw, tbuffer_load_format_x/xy/xyz and buffer_store_format_x..xyzw against values measured on RDNA. Co-Authored-By: Claude --- .../tests/execution/BufferFormat.cpp | 188 ++++++++++++++++++ .../src/SpirvMemory/SpirvFormatConvert.cpp | 44 ++-- 2 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp new file mode 100644 index 000000000..7ba31cd23 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp @@ -0,0 +1,188 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 32; +constexpr std::uint32_t Loads = 24; +constexpr std::uint32_t Stored = 7; +constexpr std::uint32_t ScratchWords = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(256) std::array Scratch{}; + +alignas(256) constexpr std::array Code{ + 0x34040086, 0x34060087, 0x34500085, 0xe0381000, 0x80023228, 0xe0381010, 0x80023628, 0xbf8c3f70, + 0xe0001000, 0x80020a28, 0xe0041004, 0x80030b28, 0xe0081008, 0x80040d28, 0xe00c100c, 0x80051028, + 0xe8381002, 0x80021428, 0xe9211004, 0x80021528, 0xe99a1000, 0x80021728, 0xe00c1008, 0x80061a28, + 0xe00c100c, 0x80071e28, 0xe0101000, 0x80083602, 0xe0141004, 0x80093702, 0xe018100c, 0x800a3602, + 0xe01c1010, 0x800b3602, 0xe01c1018, 0x80083602, 0xbf8c3f70, 0xe0781000, 0x80010a03, 0xe0781010, + 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xe0781040, 0x80011a03, 0xe0781050, + 0x80011e03, 0xbf810000, +}; + +constexpr std::uint32_t Rows[32][8] = { + {0xff807f00u, 0x80000001u, 0x7fff8001u, 0xffff0000u, 0x3f800000u, 0xbf800000u, 0x3f000000u, 0x80000000u}, + {0x00000000u, 0x7c0007c0u, 0xffe00000u, 0x7ff00000u, 0x7f800000u, 0xff800000u, 0x7fc00001u, 0x0da24260u}, + {0x81018101u, 0x00010000u, 0x00008000u, 0x0001ffffu, 0x3b808081u, 0x3b008081u, 0x37800080u, 0xb7800080u}, + {0x200001ffu, 0x3ff003ffu, 0x07ff07ffu, 0xc0000000u, 0x3f7f0000u, 0x3effffffu, 0xbf7fffffu, 0x40000000u}, + {0x52e6b438u, 0xf2a74de4u, 0x269e0d37u, 0x6513270eu, 0xbf8e4b0fu, 0xbeb2cb7fu, 0x7f800000u, 0x80000000u}, + {0x11e20b8fu, 0x3d9c1724u, 0x1738f7d9u, 0x8d116eceu, 0x3f598d83u, 0xbf800000u, 0x3e5cf891u, 0xff800000u}, + {0xf9ebdaccu, 0x3898d190u, 0x0becd7b0u, 0x8e81973eu, 0xbf0c093bu, 0x80000000u, 0x3e206dd1u, 0xbf841a62u}, + {0x301850c5u, 0x5f557203u, 0x18f135d2u, 0x8c38fb29u, 0x3e2b5fc8u, 0xbc18bc9du, 0x3f388602u, 0x3f8cebdau}, + {0x3f98e277u, 0xcb5c7427u, 0x2e05319au, 0xb2f14c94u, 0xbf8b289du, 0xbc500907u, 0xbe08394eu, 0xbf8e09ecu}, + {0x2a3af4d4u, 0xc1d3fcffu, 0x5790f82eu, 0x26e87555u, 0xbe507866u, 0xbf8c9162u, 0x3f406bccu, 0xbed4d441u}, + {0x7f26144bu, 0x9474031bu, 0xcc011cddu, 0x74c9df6au, 0x80000000u, 0xbd89ebd2u, 0xbf92358fu, 0x3ec3dba1u}, + {0xd269a9a5u, 0x72158370u, 0x48db40afu, 0xb774eb52u, 0x3ee082b0u, 0xc0000000u, 0x3e93a7f6u, 0xbf3b8f97u}, + {0xbd0561e6u, 0x3f63af83u, 0x65dc9f50u, 0x6415479cu, 0xbc14cf1du, 0x40000000u, 0x3f7f2e26u, 0x3f7244a0u}, + {0x6a50df4du, 0xfc891b4au, 0x5bd86d40u, 0xaec6f024u, 0x3f98553cu, 0x3f800000u, 0x0da24260u, 0xc0000000u}, + {0x2eae05cfu, 0x43435cc5u, 0x482c9cbcu, 0x010c4759u, 0x7f800000u, 0x3e30a07bu, 0x3efd95cdu, 0x3e9c8a1bu}, + {0x0dd27a65u, 0x74e69a5du, 0xe647cb8fu, 0xdef88334u, 0x3f7946a6u, 0xbe8f43e4u, 0xbf83f15fu, 0x7fcd5c60u}, + {0x298cb3a5u, 0x1c2442f9u, 0x570dc195u, 0x99c94309u, 0x00000000u, 0x3dc2fcb8u, 0x3e976834u, 0xbf800000u}, + {0x2607679du, 0xa268aa87u, 0x4093f6deu, 0xf4998d7cu, 0xbeb4d35cu, 0x3eaaaaabu, 0x3f9f04feu, 0xbefa7653u}, + {0xbfeaa155u, 0x57b6fb7eu, 0xbd87a865u, 0x43c71b9au, 0x3effaa7du, 0xbf44357au, 0xbeb808feu, 0x3f89d3e5u}, + {0x4c4f9b06u, 0xfa7f0eabu, 0xa49636a2u, 0xdd02de92u, 0x3eaaaaabu, 0xbf000000u, 0xbec018e6u, 0x7f800000u}, + {0x5464ecc2u, 0xa2eddbbdu, 0x39194242u, 0x9cfc8652u, 0x3fa16226u, 0x3f4bb9d5u, 0x3f1fa8d4u, 0x7f800000u}, + {0xbb2313f5u, 0x076b3e36u, 0xfd56a926u, 0x0726e25cu, 0xbd93d0edu, 0xff800000u, 0xbe0c8044u, 0x3fa26b48u}, + {0x5d58c705u, 0x149e259bu, 0x38703800u, 0x1a26f889u, 0xbf800000u, 0xbd38bc43u, 0x3e92c7eeu, 0x0da24260u}, + {0xa4a45effu, 0x15b40aebu, 0xd5ab8b4du, 0xa91c2439u, 0x40000000u, 0x3f267e55u, 0xbf55f9d7u, 0xbedef3ffu}, + {0xf8be8831u, 0xb8c9817au, 0x6555abfeu, 0x7691b06fu, 0x3f94b1adu, 0xbf5ba544u, 0x3f800000u, 0xbdb87bd0u}, + {0x9c9011efu, 0xd39630d6u, 0x988af3fbu, 0xfaf55496u, 0x3f9196d5u, 0x7f800000u, 0x00000000u, 0x3f16ac02u}, + {0xbfdefc15u, 0xef02090bu, 0x23a5ef88u, 0x6f0e2289u, 0xbf4b2333u, 0xbf9d156du, 0x7f800000u, 0xff800000u}, + {0x8b5ab3eeu, 0x6b446806u, 0xd58dcdb4u, 0x218e0b7bu, 0x8da24260u, 0xbddec8cdu, 0x3f868ccbu, 0x3f8b0480u}, + {0x8825ae56u, 0x26debfdbu, 0x86048719u, 0x82b33599u, 0xc0000000u, 0x3e908208u, 0xbf69176au, 0xff800000u}, + {0x8e752fdfu, 0x0fcf31cau, 0x537390e5u, 0xaead44b0u, 0x7fdcf073u, 0xbf831638u, 0xbf2766f5u, 0x3e800000u}, + {0xc28ee907u, 0xe4ddf9b9u, 0xe998d0eeu, 0x1038f0b5u, 0x3e95cc10u, 0x3d0183cbu, 0xbdfdbfc7u, 0xbd69e789u}, + {0xb2fff17bu, 0x85f1115bu, 0xe064a114u, 0xe040015cu, 0xbf2003efu, 0x3f9384efu, 0xbf7185f8u, 0xc0000000u} +}; + +constexpr std::uint32_t Expected[32][31] = { + {0x00000000u, 0x37800080u, 0x3f000080u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbc010204u, 0xbc010204u, 0x3f7f8100u, 0x35800000u, 0x00000000u, 0xbf004020u, 0x3d787c3eu, 0xbc804020u, 0x3f800000u, 0x00000000u, 0x3effff00u, 0x3f000180u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x000000ffu, 0x80000000u, 0x00000000u, 0x0040817fu, 0x80017fffu, 0x00004000u, 0x008000ffu}, + {0x00000000u, 0x3cf800f8u, 0x3ef800f8u, 0x00000000u, 0xba800100u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbe010204u, 0x3f800000u, 0x00000000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3effe100u, 0x00000000u, 0x3f7fe100u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x000000ffu, 0x00000000u, 0x00000000u, 0x0000817fu, 0x80017fffu, 0x00000000u, 0x000000ffu}, + {0x3b808081u, 0x00000000u, 0x37800080u, 0xbf800000u, 0x00000000u, 0xb8000100u, 0xbc010204u, 0xbc010204u, 0x3c010204u, 0x00000000u, 0x3f010181u, 0x00000000u, 0x38000000u, 0x3f00c060u, 0x3e406030u, 0x3d004020u, 0x37800080u, 0x3f800000u, 0x00000000u, 0x3f000080u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0xbf800000u, 0x00000001u, 0x00010081u, 0x00000000u, 0x00000000u, 0x00400080u, 0x00000000u, 0x00000101u}, + {0x3f800000u, 0x3c7fc100u, 0x3e7fc100u, 0x3d7fe200u, 0x3d7fe200u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbf010204u, 0x3e000080u, 0x3ffe0000u, 0x44000000u, 0x3f800000u, 0x00000000u, 0xbf800000u, 0x3f4000c0u, 0x00000000u, 0x3cffe100u, 0x3cffe100u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x000000feu, 0x00007fffu, 0x00000000u, 0x00813f7fu, 0x3fff7f7fu, 0x7fff8001u, 0xff007ffeu}, + {0x3e60e0e1u, 0x3e9bc89cu, 0x3f72a7f3u, 0x3dd371a7u, 0x3e9a7935u, 0x3e9c3938u, 0x3de1c387u, 0x3e9d3a75u, 0x3e193265u, 0x3f4b972eu, 0x3ea5cca6u, 0x43c80000u, 0x41d20000u, 0x3de07038u, 0x3f56eb76u, 0x3f174ba6u, 0x3eca26cau, 0x3e1c389cu, 0x3e1a789au, 0x3d5370d3u, 0x421c0000u, 0x3f800000u, 0x00000000u, 0x41600000u, 0x00000000u, 0xffff0000u, 0x00000000u, 0x007fd481u, 0xd34d8001u, 0x00007fffu, 0x00ff0000u}, + {0x3f0f8f90u, 0x3db920b9u, 0x3e7670f6u, 0xbd827105u, 0x3e39c174u, 0x3f5d9dbbu, 0xbec99326u, 0x3f5dbb77u, 0x3e091224u, 0xbf67cf9fu, 0x3d8f108fu, 0x46480000u, 0x3f040000u, 0xbe627139u, 0x3e824121u, 0x3f0f47a4u, 0x3f0d118du, 0x3edd9cdeu, 0x3db9c0bau, 0x3f77d9f8u, 0x42dc0000u, 0x3f800000u, 0x00000000u, 0xc2480000u, 0x000000d9u, 0x373e0000u, 0x00000000u, 0x001b816cu, 0x80016cc6u, 0x80011b9fu, 0x003700d9u}, + {0x3f4ccccdu, 0x3f5190d2u, 0x3e6260e2u, 0xbea14143u, 0x3dbec17eu, 0xbf5185a3u, 0x3ef9f3e8u, 0xbf53a74fu, 0xbf800000u, 0xbf65cb97u, 0x3f79ebfau, 0x3b200000u, 0x3e340000u, 0xbf1a4d27u, 0xbf0542a1u, 0xbe446231u, 0x3f0e818fu, 0x3f173e97u, 0x3d3ec0bfu, 0x3f57b0d8u, 0xc2d20000u, 0x3f800000u, 0x00000000u, 0x42780000u, 0x00000000u, 0x281b0000u, 0x00000000u, 0x001400bbu, 0x0000b9fcu, 0x8001140eu, 0x00280000u}, + {0x3f45c5c6u, 0x3ee406e4u, 0x3ebeaabfu, 0x3ed749afu, 0x3e47898fu, 0xbd1ae136u, 0x3ea54a95u, 0xbd214285u, 0x3ee1c387u, 0xbf69d3a7u, 0x3e4060c0u, 0x3c060000u, 0x3d5c0000u, 0x3ec562b1u, 0xbf767b3eu, 0xbeff7fc0u, 0x3f0c388cu, 0x3f7b29fbu, 0x3dc788c8u, 0x3e5748d7u, 0xc0a00000u, 0x3f800000u, 0x00000000u, 0x42240000u, 0x0000002bu, 0xb8850000u, 0x00000000u, 0x005cff15u, 0xfecf156cu, 0x7fff5c42u, 0xffb8002bu}, + {0x3eeeeeefu, 0x3ee84ee8u, 0x3f4b5ccbu, 0x3ec6698du, 0x3eb81570u, 0x3f192932u, 0xbf59b367u, 0x3f193265u, 0xbdf1e3c8u, 0xbf1d3a75u, 0x3e7e60feu, 0x404e0000u, 0x3f1c0000u, 0xbf44e271u, 0xbf647239u, 0xbc607038u, 0x3f32f1b3u, 0x3e992899u, 0x3e3814b8u, 0x3e4668c6u, 0x42980000u, 0x3f800000u, 0x00000000u, 0xc2d80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00effe81u, 0xfe608001u, 0x8001eef9u, 0x00000000u}, + {0x3f54d4d5u, 0x3f7cfffdu, 0x3f41d3c2u, 0xbd7a41f5u, 0x3f2f215eu, 0x3f6aabd5u, 0x3f2b56adu, 0x3f6bd7afu, 0xbe418306u, 0x3e993265u, 0x3e28e8a9u, 0x41fe0000u, 0x3cfe0000u, 0x3ed46a35u, 0xbf21d0e8u, 0xbf2ed76cu, 0x3e1ba09cu, 0x3eeaaaebu, 0x3eaf20afu, 0x3f782ef8u, 0x42ea0000u, 0x3f800000u, 0x00000000u, 0x42aa0000u, 0x00000000u, 0xc06b0000u, 0x00000000u, 0x005f81e6u, 0x8001e5f1u, 0xcacb6035u, 0x00c00000u}, + {0x3e969697u, 0x3c46c0c7u, 0x3f147494u, 0x3e66e9ceu, 0xbecffda0u, 0xbe825905u, 0x3f55ab57u, 0xbe850a14u, 0xbeddbb77u, 0x3f69d3a7u, 0x3efe4cfeu, 0x3e360000u, 0x45000000u, 0x3e164b26u, 0x3f42e171u, 0xbce07038u, 0x3ee992eau, 0x3f5f6adfu, 0x3f4c01ccu, 0x3de6e8e7u, 0xc2040000u, 0x3f800000u, 0x00000000u, 0x42d40000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0081f700u, 0xf7610000u, 0x30f78001u, 0x62000000u}, + {0x3f25a5a6u, 0x3f037083u, 0x3ee42ae4u, 0x3f015f03u, 0x3f11b723u, 0xbe25714bu, 0x3f254a95u, 0xbe2952a5u, 0x3f69d3a7u, 0xbf13264du, 0x3f5269d2u, 0x3ee00000u, 0x3d600000u, 0x3f52e975u, 0xbf4b65b3u, 0x3f1349a5u, 0x3f3774b7u, 0x3f6b52ebu, 0x3e91b692u, 0x3e815e81u, 0xc1a80000u, 0x3f800000u, 0x00000000u, 0x42a40000u, 0x00000070u, 0x49d40000u, 0x00000000u, 0x00258138u, 0x80013820u, 0xa23924eau, 0x004a0070u}, + {0x3f66e6e7u, 0x3f2f83b0u, 0x3e7d8cfeu, 0xbf416183u, 0x3f4bb997u, 0x3f0f391eu, 0xbf499326u, 0x3f0f1e3cu, 0x3e2952a5u, 0x3f499326u, 0x3f3d05bdu, 0x47060000u, 0x40ea0000u, 0x3f7379bdu, 0x3f2c562bu, 0xbdc06030u, 0x3ec82ac8u, 0x3e8f388fu, 0x3ecbb8ccu, 0x3f1f509fu, 0x428e0000u, 0x3f800000u, 0x00000000u, 0xc2c80000u, 0x00000000u, 0xff2dffffu, 0x00000000u, 0x007f7fffu, 0x7ffffed6u, 0x79217f96u, 0xf1feff00u}, + {0x3e9a9a9bu, 0x3dda50dau, 0x3f7c89fdu, 0x3f5a81b5u, 0x3f37b16fu, 0xbdfdc1fcu, 0x3e912245u, 0xbe010204u, 0xbee9d3a7u, 0xbf254a95u, 0x3ed4a0d5u, 0x3e940000u, 0x3a460000u, 0xbeb359adu, 0x3ddc6e37u, 0xbf2dd6ebu, 0x3f2ec6afu, 0x3f7024f0u, 0x3eb7b0b8u, 0x3eda80dbu, 0xc1800000u, 0x3f800000u, 0x00000000u, 0x42100000u, 0x000000ffu, 0x0000ffffu, 0x00000000u, 0x00007f7fu, 0x7fff7fffu, 0x80010000u, 0x0000ffffu}, + {0x3f4fcfd0u, 0x3eb98abau, 0x3e868687u, 0xbf46898du, 0x3f105921u, 0x3f0eb31du, 0x3f3366ceu, 0x3f0f1e3cu, 0x3dc18306u, 0x3c010204u, 0x3e3ab8bbu, 0x418a0000u, 0x38d60000u, 0x3f67f3fau, 0xbe7e7f40u, 0xbf0b45a3u, 0x3b860086u, 0x3e8eb28fu, 0x3e905890u, 0x3f1cbc9du, 0x428e0000u, 0x3f800000u, 0x00000000u, 0x42b20000u, 0x000000ffu, 0x7eca2c28u, 0x00000000u, 0x003f167fu, 0x16147fffu, 0x27223f65u, 0x4e7e2cffu}, + {0x3ecacacbu, 0x3f1a5d9au, 0x3ee9cceau, 0xbed1c5a4u, 0xbe4dc99cu, 0xbf7999f3u, 0x3ed1a347u, 0xbf7bf7f0u, 0xbd810204u, 0xbe891224u, 0x3d5d20ddu, 0x3cba0000u, 0x41a60000u, 0xbf4de6f3u, 0x3e9e4f28u, 0x3edd6eb7u, 0x3f5ef8dfu, 0x3f033483u, 0x3f6647e6u, 0x3f4b8fccu, 0xc2fa0000u, 0x3f800000u, 0x00000000u, 0x42500000u, 0x000000f8u, 0x00000000u, 0x00000000u, 0x0081dc7cu, 0xdc2f7ca2u, 0x00008001u, 0x000000f8u}, + {0x3f25a5a6u, 0x3e85f286u, 0x3de120e1u, 0xbef9adf3u, 0x3f2e1b5cu, 0x3f06130cu, 0x3d912245u, 0x3f070e1cu, 0xbeddbb77u, 0xbf4f9f3eu, 0x3e2630a6u, 0x3df20000u, 0x41100000u, 0xbe365b2eu, 0xbed46a35u, 0xbf345a2du, 0x3f19c99au, 0x3e861286u, 0x3eae1aaeu, 0x3f4195c2u, 0x42860000u, 0x3f800000u, 0x00000000u, 0x41100000u, 0x00000000u, 0x4bb4185fu, 0x00000000u, 0x00260c00u, 0x0c300000u, 0x800125dau, 0x004b1800u}, + {0x3f1d9d9eu, 0x3f2a87abu, 0x3f2268a2u, 0xbd922124u, 0x3f012702u, 0xbf6509cau, 0x3f79f3e8u, 0xbf67cf9fu, 0xbf4f9f3eu, 0xbdc18306u, 0x3e181c98u, 0x3d0e0000u, 0x422a0000u, 0xbe466332u, 0x3f6cf67bu, 0xbf506834u, 0x3f7499f5u, 0x3f0d7c8du, 0x3e812681u, 0x3f76def7u, 0xc2e60000u, 0x3f800000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0xffff5555u, 0x00000000u, 0x007f2ad3u, 0x2aaad2ccu, 0xc1637fffu, 0x00ff5500u}, + {0x3eaaaaabu, 0x3f7b7efbu, 0x3eaf6cafu, 0xbf2f375eu, 0xbf04f30au, 0x3e5cd1bau, 0xbf4d9b36u, 0x3e59b367u, 0xbee5cb97u, 0x3f070e1cu, 0x3f3feac0u, 0x3efc0000u, 0x45be0000u, 0x3f2ad56bu, 0xbf2c562bu, 0xbb804020u, 0x3e878e88u, 0x3ddcd0ddu, 0x3f3d87beu, 0x3f2865a8u, 0x41d80000u, 0x3f800000u, 0x00000000u, 0xc2cc0000u, 0x0000007fu, 0x00000000u, 0x00000000u, 0x00d29f3fu, 0x9de63feau, 0x7fffd1feu, 0xff00007fu}, + {0x3cc0c0c1u, 0x3d6ab0ebu, 0x3f7a7ffau, 0x3eda89b5u, 0xbf36d56eu, 0xbe85b90bu, 0xbf5dbb77u, 0xbe891224u, 0x3c810204u, 0xbe8d1a34u, 0x3e989e99u, 0x45560000u, 0x7fc20000u, 0xbefa7d3fu, 0xbd506834u, 0x3ec46231u, 0x3f5d02ddu, 0x3f5e92dfu, 0x3f2496a5u, 0x3e5a88dbu, 0xc2080000u, 0x3f800000u, 0x00000000u, 0xc2dc0000u, 0x00000055u, 0x00000000u, 0x00000000u, 0x00d0c02au, 0xc0002aaau, 0x7fffcffau, 0xff000055u}, + {0x3f42c2c3u, 0x3f5bbddcu, 0x3f22eda3u, 0x3f048509u, 0x3ee465c9u, 0xbf735de7u, 0x3f254a95u, 0xbf75ebd8u, 0xbd010204u, 0xbf499326u, 0x3ea8c8a9u, 0x3f7a0000u, 0x43760000u, 0x3ec26131u, 0x3f1dcee7u, 0x3f2351a9u, 0x3f1cfc9du, 0x3f065286u, 0x3e6464e4u, 0x3e848485u, 0xc2f40000u, 0x3f800000u, 0x00000000u, 0x42a40000u, 0x000000ffu, 0x9fa8cbb9u, 0x00000000u, 0x004f657fu, 0x65dc7fffu, 0x7fff4fd4u, 0xff9fcbffu}, + {0x3f75f5f6u, 0x3e78d8f9u, 0x3ced60edu, 0xbf2db55bu, 0xbcaa8155u, 0xbe6d21dau, 0x3f3972e6u, 0xbe71e3c8u, 0x3e993265u, 0x3d61c387u, 0x3f3b23bbu, 0x446c0000u, 0x42ce0000u, 0xbcb0582cu, 0x3ec46231u, 0xbe1c4e27u, 0x3ce4c0e5u, 0x3f625ce2u, 0x3f7d56fdu, 0x3f2926a9u, 0xc1f00000u, 0x3f800000u, 0x00000000u, 0x42b80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00ef81f7u, 0x8001f6c3u, 0x7fffee70u, 0xff000000u}, + {0x3ca0a0a1u, 0x3e166c96u, 0x3da4f0a5u, 0x3ee001c0u, 0x3ee1c1c4u, 0xbd6ee1deu, 0xbf6fdfbfu, 0xbd810204u, 0x3e993265u, 0x3e51a347u, 0x3ebab0bbu, 0x43360000u, 0x3f880000u, 0xbefb7dbfu, 0xbf67f3fau, 0x3f6af57bu, 0x3dd130d1u, 0x3f7889f9u, 0x3e61c0e2u, 0x3e6000e0u, 0xc1000000u, 0x3f800000u, 0x00000000u, 0xc2ee0000u, 0x00000000u, 0x49640000u, 0x00000000u, 0x0024fa81u, 0xfa3a8001u, 0x000024b2u, 0x00490000u}, + {0x3f800000u, 0x3d2eb0afu, 0x3dada0aeu, 0xbf6967d3u, 0xbea95553u, 0x3e90e522u, 0x3ee5cb97u, 0x3e912245u, 0x3e61c387u, 0xbf2f5ebdu, 0x3f24a4a5u, 0x3dd60000u, 0x45020000u, 0xbf00c060u, 0x3f0bc5e3u, 0xbf5b6db7u, 0x3f291ca9u, 0x3e10e491u, 0x3f55abd6u, 0x3f0b4d8bu, 0x42100000u, 0x3f800000u, 0x00000000u, 0x42640000u, 0x000000ffu, 0x0000a67eu, 0x00000000u, 0x0096537fu, 0x533f7fffu, 0xc8439504u, 0x0000a6ffu}, + {0x3e44c4c5u, 0x3f017a81u, 0x3f38c9b9u, 0xbf280550u, 0x3f4aab95u, 0xbf1f233eu, 0x3f5fbf7fu, 0xbf214285u, 0xbf5fbf7fu, 0x3f6ddbb7u, 0x3f78bef9u, 0x3af40000u, 0x3a600000u, 0x3dc46231u, 0xbe3c5e2fu, 0xbe6a753bu, 0x3eed22edu, 0x3f306fb0u, 0x3ecaaacbu, 0x3f2bfeacu, 0xc2a00000u, 0x3f800000u, 0x00000000u, 0x42de0000u, 0x000000ffu, 0xffff0000u, 0x00000000u, 0x007f937fu, 0x922e7fffu, 0xf4787fffu, 0x00ff00ffu}, + {0x3f6feff0u, 0x3e4358c3u, 0x3f5396d4u, 0xbdc05181u, 0xbf4eed9eu, 0x3f292d52u, 0xbf55ab57u, 0x3f2952a5u, 0xbdb162c6u, 0xbd418306u, 0x3f1c909du, 0x39ac0000u, 0x3d8c0000u, 0x3f77fbfeu, 0x3c004020u, 0x3f64f279u, 0x3f7af5fbu, 0x3ea92ca9u, 0x3f188a99u, 0x3f73fbf4u, 0x42a80000u, 0x3f800000u, 0x00000000u, 0xc2d40000u, 0x000000ffu, 0x0000ffffu, 0x00000000u, 0x00007f7fu, 0x7fff7fffu, 0x4b550000u, 0x9600ffffu}, + {0x3da8a8a9u, 0x3d10b091u, 0x3f6f02efu, 0xbe03c108u, 0x3e8e951du, 0x3e8a2514u, 0xbf6fdfbfu, 0x3e891224u, 0x3de1c387u, 0x3f5fbf7fu, 0x3f3fdec0u, 0x3a160000u, 0x38820000u, 0x3d28542au, 0xbe024121u, 0xbbc06030u, 0x3ede1cdeu, 0x3e0a248au, 0x3e0e948fu, 0x3f6f88f0u, 0x42080000u, 0x3f800000u, 0x00000000u, 0xc2ee0000u, 0x00000000u, 0xffff0000u, 0x00000000u, 0x007f819bu, 0x80019a6fu, 0x80017fffu, 0x00ff0000u}, + {0x3f6eeeefu, 0x3ed00cd0u, 0x3ed688d7u, 0xbec93192u, 0xbea9cd54u, 0x3db7b16fu, 0x3f77efe0u, 0x3db162c6u, 0xbf65cb97u, 0x3e850a14u, 0x3f0b5a8bu, 0x36c00000u, 0x391a0000u, 0xbd104824u, 0xbf2a552bu, 0x3eb55aadu, 0x3e063886u, 0x3d37b0b8u, 0x3f558dd6u, 0x3f4db4ceu, 0x41300000u, 0x3f800000u, 0x00000000u, 0x42f60000u, 0x00000000u, 0xffff0000u, 0x00000000u, 0x007ff200u, 0xf2140000u, 0x7fff7fffu, 0xffff0000u}, + {0x3eacacadu, 0x3f3fdbc0u, 0x3e1b789bu, 0xbf71cfe4u, 0xbf73f9e8u, 0x3ed665adu, 0xbf4f9f3eu, 0x3ed5ab57u, 0xbf1b366du, 0xbf7dfbf8u, 0x3f082588u, 0x7fb60000u, 0x3fae0000u, 0xbf556ab5u, 0x3f35daedu, 0x3e824121u, 0x3f02b383u, 0x3e5664d6u, 0x3f060486u, 0x3f071987u, 0x42540000u, 0x3f800000u, 0x00000000u, 0xc2ce0000u, 0x00000000u, 0x00004841u, 0x00000000u, 0x008c2481u, 0x24208001u, 0x80018b75u, 0x00004800u}, + {0x3f5fdfe0u, 0x3e4728c7u, 0x3d7cf0fdu, 0xbf5e37bcu, 0x3f26e74eu, 0x3f096113u, 0xbf214285u, 0x3f091224u, 0xbf274e9du, 0xbf254a95u, 0x3f0e758eu, 0x3b940000u, 0x3bcc0000u, 0xbd844221u, 0x3f25d2e9u, 0x3ee773bau, 0x3f2eadafu, 0x3e896089u, 0x3ea6e6a7u, 0x3f10e591u, 0x42880000u, 0x3f800000u, 0x00000000u, 0xc2a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00ad8100u, 0x80010000u, 0x2000ac4du, 0x40000000u}, + {0x3ce0e0e1u, 0x3f79b9fau, 0x3f64dde5u, 0xbebc4979u, 0xbe334167u, 0xbdf4b1e9u, 0xbf172e5du, 0xbe010204u, 0x3ee1c387u, 0x3e010204u, 0x3f428ec3u, 0x3b720000u, 0x3f7e0000u, 0x3f03c1e1u, 0xbe0c4623u, 0x3da05028u, 0x3d81c082u, 0x3f70b5f1u, 0x3f6998eau, 0x3f50eed1u, 0xc1800000u, 0x3f800000u, 0x00000000u, 0xc2960000u, 0x0000004bu, 0x00000818u, 0x00000000u, 0x00f00425u, 0x040c2573u, 0xf8b1f024u, 0x0000084bu}, + {0x3ef6f6f7u, 0x3d8ad88bu, 0x3f05f186u, 0xbf3dd97cu, 0xbe7ce1fau, 0x3c2e015cu, 0x3f3972e6u, 0x3c010204u, 0x3f010204u, 0xbe810204u, 0x3f32ffb3u, 0x3ab60000u, 0x44440000u, 0x3f3ddeefu, 0xbc004020u, 0xbed168b4u, 0x3f6040e0u, 0x3bae00aeu, 0x3f6064e0u, 0x3f2114a1u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x42b80000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00887fb1u, 0x7fffafffu, 0x8001873eu, 0x0000ff00u} +}; + +constexpr const char* Names[Loads + Stored] = { + "buffer_load_format_x 8_8_8_8_UNORM x", "buffer_load_format_xy 16_16_16_16_UNORM x", "buffer_load_format_xy 16_16_16_16_UNORM y", + "buffer_load_format_xyz 16_16_16_16_SNORM x", "buffer_load_format_xyz 16_16_16_16_SNORM y", "buffer_load_format_xyz 16_16_16_16_SNORM z", + "buffer_load_format_xyzw 8_8_8_8_SNORM x", "buffer_load_format_xyzw 8_8_8_8_SNORM y", "buffer_load_format_xyzw 8_8_8_8_SNORM z", + "buffer_load_format_xyzw 8_8_8_8_SNORM w", "tbuffer_load_format_x 16_UNORM x", "tbuffer_load_format_xy 10_11_11_FLOAT x", + "tbuffer_load_format_xy 10_11_11_FLOAT y", "tbuffer_load_format_xyz 2_10_10_10_SNORM x", "tbuffer_load_format_xyz 2_10_10_10_SNORM y", + "tbuffer_load_format_xyz 2_10_10_10_SNORM z", "buffer_load_format_xyzw 16_16_16_16_UNORM x", "buffer_load_format_xyzw 16_16_16_16_UNORM y", + "buffer_load_format_xyzw 16_16_16_16_UNORM z", "buffer_load_format_xyzw 16_16_16_16_UNORM w", "buffer_load_format_xyzw 8_8_8_8_SSCALED x", + "buffer_load_format_xyzw 8_8_8_8_SSCALED y", "buffer_load_format_xyzw 8_8_8_8_SSCALED z", "buffer_load_format_xyzw 8_8_8_8_SSCALED w", + "scratch dword 0 (store_format_x 8_8_8_8_UNORM)", "scratch dword 1 (store_format_xy 16_16_16_16_UNORM low)", "scratch dword 2 (store_format_xy 16_16_16_16_UNORM high)", + "scratch dword 3 (store_format_xyz 8_8_8_8_SNORM)", "scratch dword 4 (store_format_xyzw 16_16_16_16_SNORM low)", "scratch dword 5 (store_format_xyzw 16_16_16_16_SNORM high)", + "scratch dword 6 (store_format_xyzw 8_8_8_8_UNORM)", +}; + +constexpr std::array FormattedWord3{0x31038facu, 0x31041facu, 0x31042facu, 0x31039facu, 0x31041977u, 0x3103b80du, 0x31038facu, 0x31041facu, 0x31039facu, 0x31042facu}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes, std::uint32_t word3) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, word3}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("buffer format: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + Scratch.fill(0xaaaaaaaau); + std::vector userData(8u + 4u * FormattedWord3.size(), 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u), 0x31016facu); + std::copy(output.begin(), output.end(), userData.begin() + 4); + for (std::size_t index = 0; index < FormattedWord3.size(); ++index) { + const bool store = index >= 6u; + const auto descriptor = store ? BufferDescriptor(Scratch.data(), static_cast(Scratch.size() * 4u), FormattedWord3[index]) + : BufferDescriptor(Input.data(), static_cast(Input.size() * 4u), FormattedWord3[index]); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 8 + 4 * index); + } + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Loads; ++index) Expect(tid, Output[tid * Results + index], Expected[tid][index], Names[index]); + for (std::uint32_t index = 0; index < Stored; ++index) Expect(tid, Scratch[tid * ScratchWords + index], Expected[tid][Loads + index], Names[Loads + index]); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("buffer format tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp index 4f7727739..444bde46e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp @@ -19,6 +19,26 @@ namespace { throw std::runtime_error("SPIR-V module emission failed: " + reason); } + std::uint32_t UnormToF32Bits(SpirvEmitterState& state, std::uint32_t value, std::uint32_t bits) { + const std::uint32_t maximum = (1u << bits) - 1u; + const auto zero = EmitCompareU32Constant(state, spv::OpIEqual, value, 0u); + const auto safe = EmitSelectValueU32(state, zero, ConstantU32(state, 1u), value); + const auto msb = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), msb, GlslStd450(state), GLSLstd450FindUMsb, safe); + const auto leading = EmitBinaryU32(state, spv::OpISub, ConstantU32(state, bits - 1u), msb); + const auto high = EmitBinaryU32(state, spv::OpShiftLeftLogical, safe, leading); + const auto low = EmitBinaryU32(state, spv::OpShiftRightLogical, safe, EmitBinaryU32(state, spv::OpISub, ConstantU32(state, bits), leading)); + const auto rotated = EmitAndConstant(state, EmitOrU32(state, high, low), maximum); + auto pattern = EmitBinaryU32(state, spv::OpShiftLeftLogical, rotated, ConstantU32(state, 32u - bits)); + for (std::uint32_t shift = bits; shift < 32u; shift *= 2u) { + pattern = EmitOrU32(state, pattern, EmitBinaryU32(state, spv::OpShiftRightLogical, pattern, ConstantU32(state, shift))); + } + const auto round = EmitAndConstant(state, EmitBinaryU32(state, spv::OpShiftRightLogical, pattern, ConstantU32(state, 7u)), 1u); + const auto rounded = EmitAddU32(state, EmitBinaryU32(state, spv::OpShiftRightLogical, pattern, ConstantU32(state, 8u)), round); + const auto exponent = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitAddU32(state, msb, ConstantU32(state, 126u - bits)), ConstantU32(state, 23u)); + return EmitSelectValueU32(state, zero, ConstantU32(state, 0u), EmitAddU32(state, exponent, rounded)); + } + } std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value) { @@ -71,24 +91,16 @@ std::uint32_t NormalizeFormatComponent(SpirvEmitterState& state, const SpirvBuff state.module.AddFunction(spv::OpConvertSToF, TypeF32(state), value, signedRaw); return EmitBitcastF32ToU32(state, value); } - case SpirvFormatComponentType::Unorm: { - const auto value = state.module.AllocateId(); - const auto normalized = state.module.AllocateId(); - const auto maxValue = static_cast((1u << bits) - 1u); - state.module.AddFunction(spv::OpConvertUToF, TypeF32(state), value, raw); - state.module.AddFunction(spv::OpFDiv, TypeF32(state), normalized, value, ConstantF32Value(state, maxValue)); - return EmitBitcastF32ToU32(state, normalized); - } + case SpirvFormatComponentType::Unorm: + return UnormToF32Bits(state, raw, bits); case SpirvFormatComponentType::Snorm: { - const auto signedRaw = EmitTBufferBitcastU32ToI32(state, raw); - const auto value = state.module.AllocateId(); - const auto normalized = state.module.AllocateId(); + const std::uint32_t maximum = (1u << (bits - 1u)) - 1u; + const auto negative = Binary(state, spv::OpSLessThan, TypeBool(state), EmitTBufferBitcastU32ToI32(state, raw), EmitTBufferBitcastU32ToI32(state, ConstantU32(state, 0u))); + const auto magnitude = EmitSelectValueU32(state, negative, Unary(state, spv::OpSNegate, TypeU32(state), raw), raw); const auto clamped = state.module.AllocateId(); - const auto maxValue = static_cast((1u << (bits - 1u)) - 1u); - state.module.AddFunction(spv::OpConvertSToF, TypeF32(state), value, signedRaw); - state.module.AddFunction(spv::OpFDiv, TypeF32(state), normalized, value, ConstantF32Value(state, maxValue)); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMax, normalized, ConstantF32Value(state, -1.0f)); - return EmitBitcastF32ToU32(state, clamped); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, magnitude, ConstantU32(state, maximum)); + const auto sign = EmitSelectValueU32(state, negative, ConstantU32(state, 0x80000000u), ConstantU32(state, 0u)); + return EmitOrU32(state, sign, UnormToF32Bits(state, clamped, bits - 1u)); } case SpirvFormatComponentType::Float: if (bits == 32u) { From 5a6e2b8eb574a2b9acb6db84f67bae2fa3f8fa6b Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 29 Sep 2026 19:08:10 +0200 Subject: [PATCH 009/203] fix(shader): packed pixel ancillary as SampleId and Layer, gl_Layer via Geometry, Z export formats Bit-field extracts of the packed ancillary VGPR become gl_SampleID and gl_Layer reads. A pixel shader reading gl_Layer needs the Geometry capability instead of ShaderLayer and SPIR-V 1.5, and the Geometry capability is available whenever the geometryShader feature is. 32-bit SPI_SHADER_Z_FORMATs and 32_ABGR are accepted with the depth and sample-mask export enables. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/src/VulkanDevice.cpp | 4 ++ .../libSceAgcDriver/Graphics/src/State.cpp | 23 ++++-- .../Optimization/src/ConstantFolder.cpp | 70 +++++++++++++++++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 5 +- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 98d526ccb..c2309ce3b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -852,6 +852,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); enabled.shaderResourceMinLod = available.shaderResourceMinLod; if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); + enabled.sampleRateShading = available.sampleRateShading; + enabled.geometryShader = available.geometryShader; + if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); + enabled.shaderClipDistance = available.shaderClipDistance; if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); // Bindless image tables index an image array with a wave-uniform runtime slot. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 48b9a1e9c..c2c508b37 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -72,6 +72,19 @@ constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; // Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space. constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x0c000000u); +bool zFormatSupported(std::uint32_t format) { + return format == 0u || format == 1u || format == 2u || format == 3u || format == 9u; +} + +std::uint32_t shaderControlMask(std::uint32_t zFormat) { + constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t maskExportEnable = 0x100u; + auto mask = ShaderControlMask; + if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; + if (zFormat == 9u) mask &= ~maskExportEnable; + return mask; +} + // Debug aid: APS5_IGNORE_DEPTH_TEST=1 renders depth- and stencil-tested draws without a depth // target as if their tests always passed (wrong occlusion, but the draws run), so stages that // depth-test everything can exercise the draw paths before depth targets exist. @@ -469,7 +482,7 @@ State DecodeState(const QueueState& queue) { Require((depthControl & 8u) == 0 || result.depth.has_value(), "depth bounds without a depth surface"); Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); } - zero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); + zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); zero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x292, ScanModeMask, "scan conversion mode"); @@ -515,7 +528,7 @@ State DecodeState(const QueueState& queue) { // CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written. if (const auto colorControl = read(cx, 0x202); !colorControlSupported(colorControl, result.hasColorTarget)) throw std::runtime_error(colorControlMessage(colorControl)); - zero(cx, 0x1c4, ~0u, "depth or sample-mask export"); + zero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); const auto exportFormat = result.hasColorTarget ? read(cx, 0x1c5) : 0u; APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat); // SPI_SHADER_POS_FORMAT: POS0 must be a 4-component position; later vectors carry the misc/clip @@ -758,7 +771,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = require((word & 8u) == 0 || surface, "depth bounds without a depth surface"); !reason.empty()) return reason; if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason; } - if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; + std::uint32_t zFormat = 0; + static_cast(value(cx, 0x1c4, zFormat)); + if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; @@ -775,7 +790,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word); - if (auto reason = nonzero(cx, 0x1c4, ~0u, "depth or sample-mask export"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue); for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) { if (find(cx, offset) != cx.end()) continue; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 8f87d21de..ac7bcfdcd 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -2,8 +2,12 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include +#include #include #include +#include +#include +#include namespace ShaderRecompiler { namespace { @@ -181,12 +185,78 @@ bool foldCompositeExtract(IrBuilder& builder, IrValue& inst, IrOpcode construct, return false; } +void collectUses(IrValue& value, std::vector& uses) { + for (const IrUse& use : value.OperandUses()) { + if (use.user->Opcode() == IrOpcode::Identity) { + collectUses(*use.user, uses); + } else { + uses.push_back(use); + } + } +} + +bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { + auto& kind = resolveArg(ancillary, 0); + if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + return false; + } + std::vector uses; + collectUses(ancillary, uses); + if (uses.empty()) { + return false; + } + IrBlock& block = *ancillary.Parent(); + const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + IrValue& created = program.CreateValue(opcode, IrType::U32); + for (IrValue* argument : arguments) { + created.AddArgument(argument); + } + block.InsertInstructionBefore(&ancillary, &created); + return created; + }; + std::array fields {}; + const auto field = [&](std::size_t index) -> IrValue& { + if (fields[index] == nullptr) { + const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; + fields[index] = &emit(IrOpcode::GetBuiltin, {&builder.Constant(static_cast(builtin)), &builder.Constant(0u)}); + } + return *fields[index]; + }; + constexpr std::array, 2> ranges {{{8u, 4u}, {16u, 13u}}}; + IrValue* packed = nullptr; + for (const IrUse& use : uses) { + IrValue& user = *use.user; + if ((user.Opcode() == IrOpcode::BitFieldUExtract || user.Opcode() == IrOpcode::BitFieldSExtract) && use.operand == 0u) { + auto& offset = resolveArg(user, 1); + auto& count = resolveArg(user, 2); + if (isImmediate(offset, IrType::U32) && isImmediate(count, IrType::U32) && count.ImmediateU32() != 0u) { + const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { + return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.first + candidate.second - offset.ImmediateU32(); + }); + if (range != ranges.end()) { + user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); + user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); + continue; + } + } + } + if (packed == nullptr) { + IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, {&field(0u), &builder.Constant(ranges[0].first)}); + IrValue& layer = emit(IrOpcode::ShiftLeftLogical32, {&field(1u), &builder.Constant(ranges[1].first)}); + packed = &emit(IrOpcode::BitwiseOr32, {&sample, &layer}); + } + user.ReplaceArgument(use.operand, packed); + } + return true; +} + } // namespace bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { IrBuilder builder(program); switch (value.Opcode()) { case IrOpcode::Phi: return foldPhi(value); + case IrOpcode::GetBuiltin: return lowerPackedAncillary(program, builder, value); case IrOpcode::SelectU1: case IrOpcode::SelectF32: case IrOpcode::SelectU32: return foldSelect(value); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9b56e98ae..515f6d256 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -132,10 +132,13 @@ void DefineModule(SpirvEmitterState& state) { if (state.cullDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityCullDistance); } - if (state.layerVariable != 0 || InputVariableForKind(state, StageInputKind::Layer) != 0) { + if (state.layerVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderLayer); } + if (InputVariableForKind(state, StageInputKind::Layer) != 0) { + state.module.EmitCapability(spv::CapabilityGeometry); + } if (state.viewportIndexVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderViewportIndex); From 3d014251a54df7d006da1ab746eb6eaa4b5342d4 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 4 Oct 2026 10:27:07 +0200 Subject: [PATCH 010/203] fix(shader): lower only exact sample id and layer field extracts of the packed pixel ancillary Any other live use of the packed register keeps throwing, as on main, instead of reading SampleId << 8 | Layer << 16 with the other bits zero. The field range check no longer underflows for an extract that starts past a field's end. Adds the pixel_ancillary recompiler test. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 +++ core/libs/tests/PixelAncillary.cpp | 64 +++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 57 +++++++---------- 3 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 core/libs/tests/PixelAncillary.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b373847f0..4388e8574 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -758,6 +758,14 @@ if(BUILD_TESTING) set_tests_properties(image_sample_gather PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(pixel_ancillary_tests ${LIBS_INCLUDE_DIR}/tests/PixelAncillary.cpp) + target_link_libraries(pixel_ancillary_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(pixel_ancillary_tests) + add_test(NAME pixel_ancillary COMMAND pixel_ancillary_tests) + if(WIN32) + set_tests_properties(pixel_ancillary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_indexed_draw_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndexedDraw.cpp) target_include_directories(guest_indexed_draw_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc) diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp new file mode 100644 index 000000000..2d6f10dad --- /dev/null +++ b/core/libs/tests/PixelAncillary.cpp @@ -0,0 +1,64 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/ShaderInfoCollector.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; +static void Require(bool value) { if (!value) throw std::runtime_error("packed pixel ancillary regression"); } +static IrValue& Build(IrProgram& program, IrOpcode opcode, std::uint32_t offset, std::uint32_t count) { + program.Resources().stage = IrShaderStage::Pixel; + program.Resources().resourceTrackingComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::PackedAncillary)), &builder.Constant(0u)}); + auto& user = opcode == IrOpcode::BitwiseOr32 ? builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset)}) + : builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset), &builder.Constant(count)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return user; +} +static void Lower(IrProgram& program) { + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + const ShaderPixelInputInfo pixel {}; + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); +} +static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { + IrProgram program; + auto& user = Build(program, opcode, offset, count); + Lower(program); + const IrValue* field = user.Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin); + Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); + Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); + Require(user.Argument(2)->Resolve()->ImmediateU32() == count); +} +static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count) { + IrProgram program; + static_cast(Build(program, opcode, offset, count)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +int main() { + Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); + Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); + Extract(IrOpcode::BitFieldUExtract, 16u, 13u, StageInputKind::Layer, 0u); + Extract(IrOpcode::BitFieldSExtract, 20u, 9u, StageInputKind::Layer, 4u); + Refused(IrOpcode::BitwiseOr32, 1u, 0u); + Refused(IrOpcode::BitFieldUExtract, 2u, 4u); + Refused(IrOpcode::BitFieldUExtract, 10u, 4u); + Refused(IrOpcode::BitFieldUExtract, 13u, 2u); + Refused(IrOpcode::BitFieldUExtract, 16u, 14u); +} diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index ac7bcfdcd..a05a6c7a8 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -202,52 +201,42 @@ bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil } std::vector uses; collectUses(ancillary, uses); - if (uses.empty()) { - return false; - } - IrBlock& block = *ancillary.Parent(); - const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { - IrValue& created = program.CreateValue(opcode, IrType::U32); - for (IrValue* argument : arguments) { - created.AddArgument(argument); - } - block.InsertInstructionBefore(&ancillary, &created); - return created; - }; std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { if (fields[index] == nullptr) { const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; - fields[index] = &emit(IrOpcode::GetBuiltin, {&builder.Constant(static_cast(builtin)), &builder.Constant(0u)}); + IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); + created.AddArgument(&builder.Constant(static_cast(builtin))); + created.AddArgument(&builder.Constant(0u)); + ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); + fields[index] = &created; } return *fields[index]; }; constexpr std::array, 2> ranges {{{8u, 4u}, {16u, 13u}}}; - IrValue* packed = nullptr; + bool lowered = false; for (const IrUse& use : uses) { IrValue& user = *use.user; - if ((user.Opcode() == IrOpcode::BitFieldUExtract || user.Opcode() == IrOpcode::BitFieldSExtract) && use.operand == 0u) { - auto& offset = resolveArg(user, 1); - auto& count = resolveArg(user, 2); - if (isImmediate(offset, IrType::U32) && isImmediate(count, IrType::U32) && count.ImmediateU32() != 0u) { - const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { - return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.first + candidate.second - offset.ImmediateU32(); - }); - if (range != ranges.end()) { - user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); - user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); - continue; - } - } + if ((user.Opcode() != IrOpcode::BitFieldUExtract && user.Opcode() != IrOpcode::BitFieldSExtract) || use.operand != 0u) { + continue; } - if (packed == nullptr) { - IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, {&field(0u), &builder.Constant(ranges[0].first)}); - IrValue& layer = emit(IrOpcode::ShiftLeftLogical32, {&field(1u), &builder.Constant(ranges[1].first)}); - packed = &emit(IrOpcode::BitwiseOr32, {&sample, &layer}); + auto& offset = resolveArg(user, 1); + auto& count = resolveArg(user, 2); + if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u) { + continue; } - user.ReplaceArgument(use.operand, packed); + const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { + return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.second && + offset.ImmediateU32() - candidate.first <= candidate.second - count.ImmediateU32(); + }); + if (range == ranges.end()) { + continue; + } + user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); + user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); + lowered = true; } - return true; + return lowered; } } // namespace From 3e65449c40171db7140578ce706cdd7b4370bd48 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Mon, 5 Oct 2026 19:07:20 +0300 Subject: [PATCH 011/203] feat(libSceAgcDriver): implement sceAgcDriverSubmitMultiDcbs and sceAgcDriverAgrSubmitMultiDcbs Both submit each DCB of the address and size arrays to the graphics queue in array order, as sceAgcDriverSubmitDcb does for one. A count of 0 is accepted without reading the arrays; null arrays with a non-zero count and arrays outside guest memory throw. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Submit/include/Dcb.hpp | 2 ++ .../prx/libSceAgcDriver/Submit/src/Dcb.cpp | 26 +++++++++-------- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp index 4639d647f..56db31822 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp +++ b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp @@ -5,5 +5,7 @@ extern "C" int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet); extern "C" int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet); +extern "C" int APS5_VABI sceAgcDriverSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count); +extern "C" int APS5_VABI sceAgcDriverAgrSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count); #endif diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index 48da441e6..6ced7913b 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -10,6 +10,18 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static int SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* sizes, std::uint32_t count) { + if (count == 0) return 0; + if (!addresses || !sizes) throw std::runtime_error("AGC driver: null multi-DCB arrays"); + AgcDriver::GuestMemory::CheckRange(addresses, count * sizeof(*addresses), alignof(std::uint32_t*)); + AgcDriver::GuestMemory::CheckRange(sizes, count * sizeof(*sizes), alignof(std::uint32_t)); + for (std::uint32_t i = 0; i < count; ++i) { + const Packet packet{addresses[i], sizes[i], 0, {}}; + AgcDriver::Submit(&packet, 0); + } + return 0; +} + extern "C" { std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords() { @@ -33,12 +45,8 @@ int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet) { return 0; } -int APS5_VABI sceAgcDriverSubmitMultiDcbs(uint32_t* const* dcb_gpu_addrs, const uint32_t* dcb_sizes_in_dwords, uint32_t count) { - (void)dcb_gpu_addrs; - (void)dcb_sizes_in_dwords; - (void)count; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count) { + return SubmitMany(dcbGpuAddrs, dcbSizesInDwords, count); } int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet) { @@ -47,11 +55,7 @@ int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet) { } int APS5_VABI sceAgcDriverAgrSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count) { - (void)dcbGpuAddrs; - (void)dcbSizesInDwords; - (void)count; - NotImplemented_nid_no_patch(__func__); - return 0; + return SubmitMany(dcbGpuAddrs, dcbSizesInDwords, count); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index e2b8d991f..50071e742 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -342,6 +342,33 @@ void testWaitFreeSubmissionTheCpuWaitsFor() { AgcDriverWaitIdle_nid_postfix(); } +void testMultiSubmissions() { + alignas(64) static volatile std::uint32_t value = 0, done = 0; + check(sceAgcDriverSubmitMultiDcbs(nullptr, nullptr, 0) == 0, "empty multi-DCB submit failed"); + check(sceAgcDriverAgrSubmitMultiDcbs(nullptr, nullptr, 0) == 0, "empty AGR multi-DCB submit failed"); + std::array invalid{0xc0017600, 0x20c, 0}; + std::array invalidAddresses{invalid.data()}; + std::array invalidSizes{2}; + expectFailure([&] { sceAgcDriverSubmitMultiDcbs(nullptr, invalidSizes.data(), 1); }); + expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), nullptr, 1); }); + expectFailure([&] { sceAgcDriverSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); }); + expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); }); + for (bool agr : {false, true}) { + value = 0; + done = 0; + auto first = writeData(&value, 1); + auto second = writeData(&value, 2); + auto third = writeData(&done, 1); + std::array addresses{first.data(), second.data(), third.data()}; + std::array sizes{static_cast(first.size()), static_cast(second.size()), static_cast(third.size())}; + const int result = agr ? sceAgcDriverAgrSubmitMultiDcbs(addresses.data(), sizes.data(), 3) : sceAgcDriverSubmitMultiDcbs(addresses.data(), sizes.data(), 3); + check(result == 0, "multi-DCB submit failed"); + waitFor(&done, 1, "the last DCB of a multi-DCB submit never ran"); + check(value == 2, "multi-DCB submit did not run its DCBs in order"); + AgcDriverWaitIdle_nid_postfix(); + } +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -375,6 +402,7 @@ int main() { testWaitFreeSubmissionQueue0WaitsOn(); testWaitFreeSubmissionBehindHeldOne(); testWaitFreeSubmissionTheCpuWaitsFor(); + testMultiSubmissions(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC driver submit tests passed"); From 8e96e48fbd225a163ee90a81cce70504c5da6174 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 18:50:25 +0200 Subject: [PATCH 012/203] fix(libkernel): report reserved virtual ranges in sceKernelVirtualQuery A range reserved with sceKernelReserveVirtualRange is registered as not readable, and sceKernelVirtualQuery only searched readable registrations, so a query inside a reservation fell back to the host and returned EACCES. Reserved ranges are now tracked until a direct or flexible mapping or an unmap covers them, and a query inside one succeeds with the reserved piece as its extent, protection 0 and is_committed clear. PPSA12544's allocator queries a reservation before committing it and treated the error as a failed commit, then wrote to the inaccessible range. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/DirectMemory/DirectMemory.cpp | 48 ++++++++++++++++++- .../libkernel/DirectMemory/DirectMemory.hpp | 1 + .../prx/libkernel/DirectMemory/Export.cpp | 7 +++ core/libs/tests/GuestMemory.cpp | 29 ++++++++++- 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 25147d08a..fc2285a5b 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -490,6 +490,31 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { } } +std::mutex g_reservationLock; +std::map g_reservations; + +void EraseReservations(const void* addr, size_t len) { + const auto start = reinterpret_cast(addr); + const auto end = start + len; + std::lock_guard lock(g_reservationLock); + auto it = g_reservations.lower_bound(start); + if (it != g_reservations.begin() && std::prev(it)->second > start) --it; + while (it != g_reservations.end() && it->first < end) { + const auto rangeStart = it->first; + const auto rangeEnd = it->second; + it = g_reservations.erase(it); + if (rangeStart < start) g_reservations.emplace(rangeStart, start); + if (rangeEnd > end) it = g_reservations.emplace(end, rangeEnd).first; + } +} + +void RecordReservation(const void* addr, size_t len) { + EraseReservations(addr, len); + const auto start = reinterpret_cast(addr); + std::lock_guard lock(g_reservationLock); + g_reservations.emplace(start, start + len); +} + void RecordProtection(const void* addr, size_t len, int prot) { const auto start = reinterpret_cast(addr); std::lock_guard lock(g_protectionLock); @@ -522,6 +547,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, } GuestAllocations::Mutation mutation; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) { + EraseReservations(*addr, len); RecordProtection(*addr, len, prot); return 0; } @@ -538,6 +564,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, throw; } *addr = mapped; + EraseReservations(mapped, len); RecordProtection(mapped, len, prot); Trace("map direct %p+0x%zx phys=0x%llx prot=0x%x flags=0x%x align=0x%zx", mapped, len, static_cast(physStart), prot, flags, alignment); return 0; @@ -548,6 +575,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { + EraseReservations(*addr, len); RecordProtection(*addr, len, prot); return 0; } @@ -560,6 +588,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { throw; } *addr = mapped; + EraseReservations(mapped, len); RecordProtection(mapped, len, prot); Trace("map anon %p+0x%zx prot=0x%x flags=0x%x", mapped, len, prot, flags); return 0; @@ -610,6 +639,7 @@ int DoMunmap(void* addr, size_t len) { else munmap(pieceAddress, pieceBytes); #endif }); + EraseReservations(addr, len); RecordProtection(addr, len, -1); return 0; } @@ -621,7 +651,11 @@ int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment) { const bool fixed = *addr != nullptr && (flags & GuestMapFixedFlag) != 0; if (fixed && mutation.Covers(*addr, len)) { constexpr int GuestMapNoOverwrite = 0x80; - if ((flags & GuestMapNoOverwrite) == 0 && RemapFixedIntoRegistered(mutation, *addr, len, 0, GuestMapFixedFlag)) return 0; + if ((flags & GuestMapNoOverwrite) == 0 && RemapFixedIntoRegistered(mutation, *addr, len, 0, GuestMapFixedFlag)) { + RecordProtection(*addr, len, -1); + RecordReservation(*addr, len); + return 0; + } mutation.RequireAvailable(*addr, len); } if (fixed) mutation.RequireAvailable(*addr, len); @@ -634,6 +668,7 @@ int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment) { throw; } *addr = mapped; + RecordReservation(mapped, len); Trace("reserve %p+0x%zx flags=0x%x align=0x%zx", mapped, len, flags, alignment); return 0; } @@ -659,6 +694,17 @@ void ForgetDirectMemory(int64_t start, size_t len) { Trace("release physical 0x%llx+0x%zx", static_cast(first), len); } +bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(g_reservationLock); + const auto next = g_reservations.upper_bound(addr); + if (next == g_reservations.begin()) return false; + const auto containing = std::prev(next); + if (addr >= containing->second) return false; + *start = containing->first; + *end = containing->second; + return true; +} + bool GuestProtection(uintptr_t addr, int* prot) { std::lock_guard lock(g_protectionLock); auto next = g_protections.upper_bound(addr); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index badcadce6..3ea3a950b 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -22,5 +22,6 @@ int DoMprotect(const void* addr, size_t len, int prot); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment); bool GuestProtection(uintptr_t addr, int* prot); +bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t* end); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 373de6c2a..622f21cc3 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -212,6 +212,13 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf // mapping at or above it: titles walk their mappings and check that a mapping covers a whole // allocation, so the answer must be the registered allocation, not a page. constexpr int findNext = 1; + std::uintptr_t reservedStart = 0; + std::uintptr_t reservedEnd = 0; + if (GuestReservation(address, &reservedStart, &reservedEnd)) { + info->start = reservedStart; + info->end = reservedEnd; + return 0; + } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); const GuestAllocations::Range* best = nullptr; for (const auto& range : lease) { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 4b6c3cbcd..908edab23 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -193,7 +193,8 @@ static void CheckFixedVirtualReservation() { Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0x10, 0) == 0); Require(reserved == requested); VirtualQueryInfo after{}; - Require(sceKernelVirtualQuery(reserved, 0, &after, sizeof(after)) != 0); + Require(sceKernelVirtualQuery(reserved, 0, &after, sizeof(after)) == 0); + Require(!after.is_committed && !after.is_direct && after.protection == 0); void* remapped = requested; Require(sceKernelMapDirectMemory(&remapped, page * 2, 3, 0x10, phys, 0) == 0); Require(remapped == requested); @@ -215,6 +216,31 @@ static void CheckFixedVirtualReservation() { Require(sceKernelMunmap(pooled, page * 2) == 0); } +static void CheckReservedRangeIsNotCommitted() { + constexpr std::size_t page = 0x4000; + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0); + const auto start = reinterpret_cast(reserved); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(reserved, 0, &info, sizeof(info)) == 0); + Require(!info.is_committed && !info.is_direct && !info.is_flexible && info.protection == 0); + Require(info.start == start && info.end == start + page * 2); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* fixed = reserved; + Require(sceKernelMapDirectMemory(&fixed, page, 3, 0x10, phys, 0) == 0); + Require(fixed == reserved); + static_cast(fixed)[0] = 7; + Require(sceKernelVirtualQuery(reserved, 0, &info, sizeof(info)) == 0); + Require(info.is_committed && info.is_direct && info.protection == 3); + Require(info.start == start && info.end == start + page); + Require(sceKernelVirtualQuery(static_cast(reserved) + page, 0, &info, sizeof(info)) == 0); + Require(!info.is_committed && info.protection == 0); + Require(info.start == start + page && info.end == start + page * 2); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + #ifdef _WIN32 static void CheckNoOverwriteRejectsHostOccupiedMapping() { constexpr std::size_t page = 0x4000; @@ -669,6 +695,7 @@ int main() { CheckInternalNamedFlexibleMapping(); CheckDirectMemoryFollowsPhysicalPages(); CheckFixedVirtualReservation(); + CheckReservedRangeIsNotCommitted(); CheckMlock(); CheckSharedDirectMemoryLifecycle(); CheckHeapAfterMappingReuse(); From b530c9b7845f76f21bf99c0dbbffb20819d528be Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:08:45 +0200 Subject: [PATCH 013/203] feat(libkernel): implement sceKernelIsStack Track the stack range of each running guest thread and answer sceKernelIsStack from the calling thread's stack (the host limits for an adopted thread) or from that registry. A mapped address outside every stack succeeds with null bounds and an unmapped one returns EACCES; the output pointers may be null. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../prx/libkernel/DirectMemory/Export.cpp | 13 +++- .../prx/libkernel/Pthread/include/Pthread.hpp | 2 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 71 ++++++++++++++++++ core/libs/tests/GuestKernelIsStack.cpp | 74 +++++++++++++++++++ 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestKernelIsStack.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2e5364b1b..79f65c06a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -136,6 +136,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_barrier PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_is_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelIsStack.cpp) + target_include_directories(guest_kernel_is_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kernel_is_stack_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kernel_is_stack_tests) + add_test(NAME guest_kernel_is_stack COMMAND guest_kernel_is_stack_tests) + set_tests_properties(guest_kernel_is_stack PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 373de6c2a..2babc2d83 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define NOMINMAX @@ -306,10 +307,14 @@ int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** en } int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { - (void)addr; - (void)start; - (void)end; - NotImplemented_nid_no_patch(__func__); + std::uintptr_t stackStart = 0; + std::uintptr_t stackEnd = 0; + if (!GuestThreadStack(reinterpret_cast(addr), &stackStart, &stackEnd)) { + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(addr, 0, &info, sizeof(info)) != 0) return SCE_KERNEL_ERROR_EACCES; + } + if (start) *start = reinterpret_cast(stackStart); + if (end) *end = reinterpret_cast(stackEnd); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index b95ca4f87..ffd954255 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -99,4 +99,6 @@ struct PthreadPrivate { PthreadPrivate() : _finished(false), _retval(nullptr), _detached(false), _adopted(false) {} }; +bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end); + #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 3dc69cbab..d4d564ea7 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,6 +44,73 @@ static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; static thread_local PthreadPrivate* currentThread = nullptr; static thread_local bool threadFinishing = false; +static std::mutex stackLock; +static std::map> liveStacks; + +static bool NativeStackLimits(std::uintptr_t* low, std::uintptr_t* high) { +#ifdef _WIN32 + ULONG_PTR lowLimit = 0; + ULONG_PTR highLimit = 0; + GetCurrentThreadStackLimits(&lowLimit, &highLimit); + *low = lowLimit; + *high = highLimit; +#else + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) != 0) + return false; + void* address = nullptr; + std::size_t size = 0; + const bool queried = pthread_attr_getstack(&attr, &address, &size) == 0; + pthread_attr_destroy(&attr); + if (!queried) + return false; + *low = reinterpret_cast(address); + *high = *low + size; +#endif + return *high > *low; +} + +static bool CurrentStack(std::uintptr_t* low, std::uintptr_t* high) { + if (currentThread && currentThread->stackAddress) { + *low = reinterpret_cast(currentThread->stackAddress); + *high = *low + currentThread->stackSize; + return true; + } + return NativeStackLimits(low, high); +} + +static void RegisterStack(PthreadPrivate* self) { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + if (!CurrentStack(&low, &high)) + throw std::runtime_error("Cannot query guest thread stack"); + std::lock_guard lock(stackLock); + liveStacks[self] = {low, high}; +} + +static void UnregisterStack(PthreadPrivate* self) { + std::lock_guard lock(stackLock); + liveStacks.erase(self); +} + +bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end) { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + if (CurrentStack(&low, &high) && address >= low && address < high) { + *start = low; + *end = high; + return true; + } + std::lock_guard lock(stackLock); + for (const auto& [thread, range] : liveStacks) { + if (address >= range.first && address < range.second) { + *start = range.first; + *end = range.second; + return true; + } + } + return false; +} #ifndef _WIN32 static thread_local std::unique_ptr adoptedThread; // pthread_exit force-unwinds through guest frames, whose personality resolves to @@ -81,6 +150,7 @@ static void finishThread(PthreadPrivate* self, void* retval) { if (threadFinishing) throw std::runtime_error("Guest thread is already finishing"); threadFinishing = true; + UnregisterStack(self); if (const auto callback = threadDtors.load()) callback(); { @@ -97,6 +167,7 @@ static void RunThread(std::unique_ptr args) { void* arg = args->arg; PthreadPrivate* self = args->self; currentThread = self; + RegisterStack(self); args.reset(); finishThread(self, entry(arg)); currentThread = nullptr; diff --git a/core/libs/tests/GuestKernelIsStack.cpp b/core/libs/tests/GuestKernelIsStack.cpp new file mode 100644 index 000000000..b2c1097d2 --- /dev/null +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); +int APS5_VABI sceKernelMapFlexibleMemory(void** addr, std::size_t len, int prot, int flags); +int APS5_VABI sceKernelMunmap(std::uint64_t addr, std::size_t len); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000d); +static int global = 0; + +static void Require(bool value) { if (!value) std::abort(); } + +struct WorkerContext { + std::atomic queried{false}; + std::atomic release{false}; + void* local = nullptr; + void* start = nullptr; + void* end = nullptr; + int result = -1; +}; + +static void* APS5_VABI Worker(void* arg) { + auto& context = *static_cast(arg); + int local = 0; + context.local = &local; + context.result = sceKernelIsStack(&local, &context.start, &context.end); + context.queried.store(true); + while (!context.release.load()) {} + return nullptr; +} + +int main() { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + Require(start <= static_cast(&local) && static_cast(&local) < end); + Require(sceKernelIsStack(&local, nullptr, nullptr) == SCE_OK); + + start = &local; + end = &local; + Require(sceKernelIsStack(&global, &start, &end) == SCE_OK); + Require(start == nullptr && end == nullptr); + + WorkerContext context; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Worker, &context, nullptr) == SCE_OK); + while (!context.queried.load()) {} + Require(context.result == SCE_OK); + Require(context.start <= context.local && context.local < context.end); + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + Require(context.end <= start || context.start >= end); + void* workerStart = nullptr; + void* workerEnd = nullptr; + Require(sceKernelIsStack(context.local, &workerStart, &workerEnd) == SCE_OK); + Require(workerStart == context.start && workerEnd == context.end); + context.release.store(true); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + constexpr std::size_t page = 0x4000; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, page, 3, 0) == SCE_OK); + Require(sceKernelIsStack(mapped, &start, &end) == SCE_OK); + Require(start == nullptr && end == nullptr); + Require(sceKernelMunmap(reinterpret_cast(mapped), page) == SCE_OK); + Require(sceKernelIsStack(mapped, &start, &end) == SCE_KERNEL_ERROR_EACCES); +} From d52a5f0169f5257e48023cbab35a3136b49e4ac3 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:38:51 +0200 Subject: [PATCH 014/203] feat(relinker): relink needed modules outside the module directories Unity titles keep modules the executable needs in Media/Modules (PPSA12544: Il2cppUserAssemblies.prx and PS5Util.prx), outside sce_module and prx, so they were left as host libraries and startup failed to load them. A DT_NEEDED module of the executable that is not in a module directory is now searched for under the input directory and relinked like the others; two ELF files with the name are ambiguous. Guest outputs keep their directory relative to the input instead of only its last component. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + core/relinker/main.cpp | 2 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 45 +++++++++-- .../tests/test_guest_needed_modules.py | 77 +++++++++++++++++++ 4 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_needed_modules.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a388ec7a3..edfe5ec1b 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -208,6 +208,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) + add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 2e3b6553e..1b4db2e0d 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -135,7 +135,7 @@ int main(const int argc, char* argv[]) { << std::filesystem::path(absPath).filename().string() << "\n" << "libs/\n *.prx\napp0/\n \n"; for (const auto& artifact : guestArtifacts) - std::cout << " " << artifact.Path.parent_path().filename().string() << "/" << artifact.Path.filename().string() << '\n'; + std::cout << " " << artifact.Path.lexically_relative(std::filesystem::path(absPath).parent_path() / "app0").generic_string() << '\n'; std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index eebb504ca..b9d9f8c10 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -26,6 +26,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (hasPrx) directories.push_back(prx); std::vector paths; std::set unmatchedExclusions = excludedModules; + const auto isElf = [](const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + path.string()); + char magic[4]{}; + stream.read(magic, 4); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + path.string()); + return stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; + }; for (const auto& directory : directories) { if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); for (const auto& entry : std::filesystem::directory_iterator(directory)) { @@ -35,14 +43,35 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path continue; } if (!entry.is_regular_file()) continue; - std::ifstream stream(entry.path(), std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string()); - if (stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path()); + if (isElf(entry.path())) paths.push_back(entry.path()); } } + if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); + std::set missingNeeded; + for (std::size_t offset = 0; offset < dynamic.DynamicSegmentData.size(); offset += 16) { + if (Io::ReadU64(dynamic.DynamicSegmentData, offset) != 1) continue; + const auto nameOffset = Io::ReadU64(dynamic.DynamicSegmentData, offset + 8); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid dependency string offset"); + const auto start = dynamic.DynStrData.begin() + static_cast(nameOffset); + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated dependency string"); + const std::string name(start, end); + if (excludedModules.contains(name)) continue; + if (std::none_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) missingNeeded.insert(name); + } + if (!missingNeeded.empty()) { + std::map found; + for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) { + if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) { + it.disable_recursion_pending(); + continue; + } + const auto name = it->path().filename().string(); + if (!it->is_regular_file() || !missingNeeded.contains(name) || !isElf(it->path())) continue; + if (!found.emplace(name, it->path()).second) throw Domain::RelinkerException("Ambiguous needed module: " + found.at(name).string() + " and " + it->path().string()); + } + for (const auto& [name, path] : found) paths.push_back(path); + } if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); if (paths.empty()) return {}; @@ -142,7 +171,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().filename().generic_string() + "/" + images[index].OutputName); + for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); std::string guestRunPath = runPath; if (!windows) { @@ -153,7 +182,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector artifacts; for (const auto index : order) { const auto& image = images[index]; - const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().filename().generic_string(); + const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory; const auto target = destination / image.OutputName; if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py new file mode 100644 index 000000000..5d3fd88dd --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -0,0 +1,77 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol, needed_libraries + + +NEEDED = b"needed.prx" + + +def executable_with_needed(): + image = main_fixture() + strings = b"\0" + NEEDED + b"\0" + image[0x4800:0x4800 + len(strings)] = strings + tags = [] + for position in range(0x4600, 0x4600 + 0x200, 16): + tag, value = struct.unpack_from(" Date: Mon, 5 Oct 2026 19:20:43 +0200 Subject: [PATCH 015/203] feat(libkernel): implement sceKernelCheckedReleaseDirectMemory Release direct memory only when the whole range is allocated: a misaligned start or length returns EINVAL, a zero length succeeds, and a range that leaves direct memory or covers an unallocated page returns ENOENT without releasing anything, as in shadPS4. The check and the release run under the pool lock. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/DirectMemory/DirectMemory.hpp | 1 + .../prx/libkernel/DirectMemory/Export.cpp | 7 ++-- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 42 +++++++++++++------ core/libs/tests/GuestMemory.cpp | 19 +++++++++ 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index badcadce6..2fd79f093 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -11,6 +11,7 @@ static constexpr size_t PS5_PAGE_SIZE = 0x4000; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); +bool DirectMemoryCheckedFree(int64_t start, size_t len); void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); void ForgetDirectMemory(int64_t start, size_t len); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 373de6c2a..949b039b5 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -284,10 +284,9 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf // --------------------------------------------------------------------------- int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { - (void)start; - (void)len; - NotImplemented_nid_no_patch(__func__); - return 0; + if (start < 0 || (static_cast(start) & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (len == 0) return 0; + return DirectMemoryCheckedFree(start, len) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int prot) { diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 1175ada11..b1e04c262 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -32,18 +32,17 @@ struct PhysicalMemoryPool { void Free(uint64_t start, size_t len) { std::lock_guard lock(_mutex); - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; - } + _free(start, len); + } + + bool CheckedFree(uint64_t start, size_t len) { + std::lock_guard lock(_mutex); + if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; + const size_t first = start / PS5_PAGE_SIZE; + for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) + if (!_used[first + i]) return false; + _free(start, len); + return true; } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { @@ -70,6 +69,21 @@ struct PhysicalMemoryPool { } private: + void _free(uint64_t start, size_t len) { + ForgetDirectMemory(static_cast(start), len); + _mark(start, len, false); + const uint64_t end = start + len; + auto it = _blocks.upper_bound(start); + if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; + while (it != _blocks.end() && it->first < end) { + const uint64_t blockStart = it->first; + const Block block = it->second; + it = _blocks.erase(it); + if (blockStart < start) _blocks[blockStart] = {start, block.type}; + if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + } + } + bool _isFree(uint64_t offset, size_t len) const { size_t first = offset / PS5_PAGE_SIZE; size_t count = len / PS5_PAGE_SIZE; @@ -103,6 +117,10 @@ void DirectMemoryFree(int64_t start, size_t len) { PhysicalMemoryPool::Instance().Free(static_cast(start), len); } +bool DirectMemoryCheckedFree(int64_t start, size_t len) { + return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); +} + bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 4b6c3cbcd..4057e22ca 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -48,6 +48,7 @@ int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelCheckedReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); int APS5_VABI sceKernelMemoryPoolReserve(void*, std::size_t, std::size_t, int, void**); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); @@ -121,6 +122,23 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckCheckedReleaseDirectMemory() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys + 1, page) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCheckedReleaseDirectMemory(phys, page + 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCheckedReleaseDirectMemory(phys, 0) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys, page * 3) == SCE_KERNEL_ERROR_ENOENT); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys + page, page) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys, page * 2) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelCheckedReleaseDirectMemory(phys, page) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys, page) == SCE_KERNEL_ERROR_ENOENT); +} + static void CheckDirectMemoryFollowsPhysicalPages() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -667,6 +685,7 @@ static void CheckDirectMemoryWriteWatch() { int main() { CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckCheckedReleaseDirectMemory(); CheckDirectMemoryFollowsPhysicalPages(); CheckFixedVirtualReservation(); CheckMlock(); From ba8c8c4b05d262951a2c1eb4141258ce8196bdb1 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 19:55:15 +0200 Subject: [PATCH 016/203] feat(libkernel): implement sceKernelGetModuleInfoFromAddr A title's libc.prx calls it from __cxa_thread_atexit with flags 2 and stores the returned module id in the destructor record. The 0x1A8-byte info reports the name, id, TLS block, init and fini procedures, eh_frame_hdr and eh_frame, and up to four load segments of the image containing the address. The id is the loader handle when the image was opened through sceKernelLoadStartModule and otherwise a stable id from the same range. Unknown flags and st_size values throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/SceTypes.hpp | 36 +++- core/libs/prx/libkernel/CMakeLists.txt | 10 + .../libkernel/Module/src/DynamicLoader.cpp | 10 + core/libs/prx/libkernel/Module/src/Module.cpp | 8 - .../prx/libkernel/Module/src/ModuleInfo.cpp | 186 ++++++++++++++++++ core/libs/tests/GuestModuleInfo.cpp | 64 ++++++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libkernel/Module/src/ModuleInfo.cpp create mode 100644 core/libs/tests/GuestModuleInfo.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index f58476bec..84a48a94f 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -199,13 +199,39 @@ struct FileStat { KernelTimespec st_birthtim; }; -struct ModuleInfo { - std::uint64_t size; - std::uint64_t info[32]; - KernelModule handle; - std::uint8_t pad[156]; +struct ModuleSegmentInfo { + std::uint64_t address; + std::uint32_t size; + std::int32_t prot; }; +struct ModuleInfoEx { + std::uint64_t st_size; + char name[256]; + KernelModule id; + std::uint32_t tls_index; + std::uint64_t tls_init_addr; + std::uint32_t tls_init_size; + std::uint32_t tls_size; + std::uint32_t tls_offset; + std::uint32_t tls_align; + std::uint64_t init_proc_addr; + std::uint64_t fini_proc_addr; + std::uint64_t reserved1; + std::uint64_t reserved2; + std::uint64_t eh_frame_hdr_addr; + std::uint64_t eh_frame_addr; + std::uint32_t eh_frame_hdr_size; + std::uint32_t eh_frame_size; + ModuleSegmentInfo segments[4]; + std::uint32_t segment_count; + std::uint32_t ref_count; +}; +static_assert(offsetof(ModuleInfoEx, id) == 0x108); +static_assert(offsetof(ModuleInfoEx, eh_frame_hdr_addr) == 0x148); +static_assert(offsetof(ModuleInfoEx, segments) == 0x160); +static_assert(sizeof(ModuleInfoEx) == 0x1A8); + struct ModuleInfoForUnwind { std::uint64_t st_size; char name[256]; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2e5364b1b..fdc0e284a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/DynamicLoader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/ModuleInfo.cpp ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Aio/src/Aio.cpp @@ -98,6 +99,15 @@ if(BUILD_TESTING) set_tests_properties(guest_dynamic_loader PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT WIN32) + add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp) + target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_module_info_tests PRIVATE libkernel libc) + add_dependencies(guest_module_info_tests guest_module_fixture) + add_test(NAME guest_module_info COMMAND guest_module_info_tests $) + set_tests_properties(guest_module_info PROPERTIES + WORKING_DIRECTORY "$") + endif() add_test_executable(guest_filesystem_tests ${LIBS_INCLUDE_DIR}/tests/GuestFilesystem.cpp) target_include_directories(guest_filesystem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_filesystem_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index e1091def8..b85934bb8 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -37,6 +37,7 @@ struct Module { std::mutex modulesMutex; std::map> modules; std::uintptr_t nextHandle = 0x20000000; +std::map imageIds; void* Symbol(Module& module, const char* name) { #ifdef _WIN32 return reinterpret_cast(GetProcAddress(static_cast(module.native), name)); @@ -123,4 +124,13 @@ int APS5_VABI dlclose_nid_postfix(void* handle) { module.reset(); return 0; } +std::int32_t ModuleIdForImage_nid_no_patch(const void* native) { + std::lock_guard lock(modulesMutex); + for (const auto& [handle, module] : modules) { + if (module->native == native) return static_cast(handle); + } + const auto [found, inserted] = imageIds.emplace(native, nextHandle); + if (inserted) ++nextHandle; + return static_cast(found->second); +} } diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 3e5ba3656..1d52dc2ea 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -84,14 +84,6 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn #endif } -int APS5_VABI sceKernelGetModuleInfoFromAddr(uint64_t addr, int n, ModuleInfo* r) { - (void)addr; - (void)n; - (void)r; - NotImplemented_nid_no_patch(__func__); - return 0; -} - KernelModule APS5_VABI sceKernelLoadStartModule(const char* module_file_name, size_t args, const void* argp, uint32_t flags, const KernelLoadModuleOpt* opt, int* res) { (void)args; (void)argp; diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp new file mode 100644 index 000000000..926f25ae5 --- /dev/null +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#ifndef _WIN32 +#include +#include +#include +#endif + +extern "C" std::int32_t ModuleIdForImage_nid_no_patch(const void* native); + +namespace { + +#ifndef _WIN32 +constexpr char GuestModuleSuffix[] = ".guest.prx"; +constexpr std::int32_t ProtRead = 1; +constexpr std::int32_t ProtWrite = 2; +constexpr std::int32_t ProtExecute = 4; + +struct ImageSearch { + std::uintptr_t address; + ModuleInfoEx* info; + bool found; +}; + +bool Contains(const dl_phdr_info& image, std::uintptr_t begin, std::uint64_t size) { + for (std::uint16_t i = 0; i < image.dlpi_phnum; ++i) { + const auto& header = image.dlpi_phdr[i]; + if (header.p_type != PT_LOAD) continue; + const std::uintptr_t start = image.dlpi_addr + header.p_vaddr; + if (begin >= start && begin - start <= header.p_memsz && size <= header.p_memsz - (begin - start)) return true; + } + return false; +} + +std::uint32_t ToU32(std::uint64_t value, const char* field) { + if (value > std::numeric_limits::max()) + throw std::runtime_error(std::string("sceKernelGetModuleInfoFromAddr: ") + field + " exceeds 32 bits"); + return static_cast(value); +} + +std::uintptr_t EhFrameAddress(const dl_phdr_info& image, std::uintptr_t header) { + if (!Contains(image, header, 8)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image"); + const auto* bytes = reinterpret_cast(header); + if (bytes[0] != 1) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr version"); + const std::uintptr_t field = header + 4; + switch (bytes[1]) { + case 0x1b: { + std::int32_t offset; + std::memcpy(&offset, bytes + 4, sizeof(offset)); + return field + static_cast(offset); + } + case 0x03: { + std::uint32_t value; + std::memcpy(&value, bytes + 4, sizeof(value)); + return value; + } + case 0x00: { + std::uint64_t value; + if (!Contains(image, header, 12)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image"); + std::memcpy(&value, bytes + 4, sizeof(value)); + return static_cast(value); + } + default: + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_ptr encoding"); + } +} + +std::uint64_t EhFrameSize(const dl_phdr_info& image, std::uintptr_t frame) { + std::uintptr_t position = frame; + for (;;) { + if (!Contains(image, position, 4)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image"); + std::uint32_t length; + std::memcpy(&length, reinterpret_cast(position), sizeof(length)); + position += 4; + if (length == 0) return position - frame; + std::uint64_t recordLength = length; + if (length == 0xffffffffu) { + if (!Contains(image, position, 8)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image"); + std::memcpy(&recordLength, reinterpret_cast(position), sizeof(recordLength)); + position += 8; + } + if (!Contains(image, position, recordLength)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame record outside the image"); + position += recordLength; + } +} + +std::string ImageName(const dl_phdr_info& image) { + std::string path = image.dlpi_name ? image.dlpi_name : ""; + if (path.empty()) { + char executable[4096]; + const auto length = ::readlink("/proc/self/exe", executable, sizeof(executable) - 1); + if (length < 0) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: cannot resolve the executable path"); + path.assign(executable, static_cast(length)); + } + std::string name = path.substr(path.find_last_of('/') + 1); + if (name.size() > sizeof(GuestModuleSuffix) - 1 && name.ends_with(GuestModuleSuffix)) + name.resize(name.size() - (sizeof(GuestModuleSuffix) - 1)); + return name; +} + +void Fill(const dl_phdr_info& image, ModuleInfoEx& info) { + const auto name = ImageName(image); + if (name.size() >= sizeof(info.name)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: module name too long: " + name); + std::memcpy(info.name, name.c_str(), name.size() + 1); + info.tls_index = ToU32(image.dlpi_tls_modid, "TLS module index"); + for (std::uint16_t i = 0; i < image.dlpi_phnum; ++i) { + const auto& header = image.dlpi_phdr[i]; + const std::uintptr_t address = image.dlpi_addr + header.p_vaddr; + if (header.p_type == PT_LOAD && info.segment_count < std::size(info.segments)) { + auto& segment = info.segments[info.segment_count++]; + segment.address = address; + segment.size = ToU32(header.p_memsz, "segment size"); + segment.prot = ((header.p_flags & PF_R) ? ProtRead : 0) | ((header.p_flags & PF_W) ? ProtWrite : 0) | ((header.p_flags & PF_X) ? ProtExecute : 0); + } else if (header.p_type == PT_TLS) { + info.tls_init_addr = address; + info.tls_init_size = ToU32(header.p_filesz, "TLS image size"); + info.tls_size = ToU32(header.p_memsz, "TLS size"); + info.tls_align = ToU32(header.p_align, "TLS alignment"); + } else if (header.p_type == PT_GNU_EH_FRAME) { + info.eh_frame_hdr_addr = address; + info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size"); + info.eh_frame_addr = EhFrameAddress(image, address); + info.eh_frame_size = ToU32(EhFrameSize(image, info.eh_frame_addr), "eh_frame size"); + } else if (header.p_type == PT_DYNAMIC) { + for (const auto* entry = reinterpret_cast(address); entry->d_tag != DT_NULL; ++entry) { + if (entry->d_tag == DT_INIT) info.init_proc_addr = image.dlpi_addr + entry->d_un.d_ptr; + else if (entry->d_tag == DT_FINI) info.fini_proc_addr = image.dlpi_addr + entry->d_un.d_ptr; + } + } + } + info.ref_count = 1; +} + +int FindImage(dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + if (!Contains(*image, search.address, 1)) return 0; + Fill(*image, *search.info); + search.found = true; + return 1; +} +#endif + +} + +extern "C" { + +int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info) { + if (!info) return SCE_KERNEL_ERROR_EFAULT; + if (flags != 2) throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported flags " + std::to_string(flags)); + if (info->st_size != sizeof(ModuleInfoEx)) + throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported st_size " + std::to_string(info->st_size)); +#ifdef _WIN32 + (void)address; + NotImplemented_nid_no_patch(__func__); + return 0; +#else + Dl_info symbol{}; + link_map* native = nullptr; + if (!dladdr1(reinterpret_cast(address), &symbol, reinterpret_cast(&native), RTLD_DL_LINKMAP) || !native) + return SCE_KERNEL_ERROR_ESRCH; + ModuleInfoEx result{}; + result.st_size = sizeof(ModuleInfoEx); + ImageSearch search{static_cast(address), &result, false}; + dl_iterate_phdr(FindImage, &search); + if (!search.found) return SCE_KERNEL_ERROR_ESRCH; + result.id = ModuleIdForImage_nid_no_patch(native); + *info = result; + return 0; +#endif +} + +} diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp new file mode 100644 index 000000000..5921931c9 --- /dev/null +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -0,0 +1,64 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +extern "C" { +void* APS5_VABI dlopen_nid_postfix(const char*, int); +void* APS5_VABI dlsym_nid_postfix(void*, const char*); +int APS5_VABI dlclose_nid_postfix(void*); +int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t, int, ModuleInfoEx*); +} +static void Require(bool value) { if (!value) std::abort(); } +template +static bool ThrowsInvalidArgument(TFunction function) { + try { + function(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} +static ModuleInfoEx Query(const void* address, int expected) { + ModuleInfoEx info{}; + info.st_size = sizeof(ModuleInfoEx); + Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(address), 2, &info) == expected); + return info; +} +int main(int argc, char** argv) { + Require(argc == 2); + void* module = dlopen_nid_postfix(argv[1], 2); + Require(module != nullptr); + const void* add = dlsym_nid_postfix(module, "GuestModuleAdd"); + Require(add != nullptr); + const auto info = Query(add, 0); + Require(info.st_size == sizeof(ModuleInfoEx)); + Require(info.id == static_cast(reinterpret_cast(module))); + Require(std::string(info.name) == std::string(argv[1]).substr(std::string(argv[1]).find_last_of("/\\") + 1)); + Require(info.segment_count > 0 && info.segment_count <= 4 && info.ref_count == 1); + bool executable = false; + for (std::uint32_t i = 0; i < info.segment_count; ++i) { + const auto& segment = info.segments[i]; + if (reinterpret_cast(add) >= segment.address && reinterpret_cast(add) - segment.address < segment.size) + executable = (segment.prot & 5) == 5; + } + Require(executable); + Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0 && info.eh_frame_addr != 0 && info.eh_frame_size != 0); + Require(info.init_proc_addr == 0 || info.init_proc_addr >= info.segments[0].address); + const auto self = Query(reinterpret_cast(&Require), 0); + Require(self.id != info.id && Query(reinterpret_cast(&Query), 0).id == self.id); + void* second = dlopen_nid_postfix(argv[1], 2); + Require(second && Query(add, 0).id == info.id); + Require(dlclose_nid_postfix(second) == 0); + int local = 0; + Query(&local, SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); + ModuleInfoEx invalid{}; + invalid.st_size = sizeof(ModuleInfoEx); + Require(ThrowsInvalidArgument([&] { sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 1, &invalid); })); + invalid.st_size = sizeof(ModuleInfoEx) - 8; + Require(ThrowsInvalidArgument([&] { sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, &invalid); })); + Require(dlclose_nid_postfix(module) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 02ea84a84..6b7eee902 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -40,6 +40,7 @@ Throughout the project, every function at every stage either **does exactly what - [gQkqkLttcpw](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature +- [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. Not implemented on Windows - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature From 3b8ef5bc4fe4001e4f627dbc00cb87974cd2980e Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:47:30 +0200 Subject: [PATCH 017/203] feat(libkernel): record exception handlers sceKernelInstallExceptionHandler and sceKernelRemoveExceptionHandler threw. They now keep one handler per allowed signal (SIGHUP, SIGILL, SIGFPE, SIGBUS, SIGSEGV, SIGUSR1, as in shadPS4): another signal or a null handler returns EINVAL, a second handler for a signal returns EAGAIN, and removing clears it. Delivery is not implemented and is recorded in the technical debt; sceKernelRaiseException still throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++++ .../prx/libkernel/System/src/Exception.cpp | 30 ++++++++++++++--- core/libs/tests/GuestExceptionHandler.cpp | 33 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 ++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestExceptionHandler.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2e5364b1b..4af255f56 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -245,6 +245,15 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_exception_handler_tests ${LIBS_INCLUDE_DIR}/tests/GuestExceptionHandler.cpp) + target_include_directories(guest_exception_handler_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_exception_handler_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_exception_handler_tests) + add_test(NAME guest_exception_handler COMMAND guest_exception_handler_tests) + if(WIN32) + set_tests_properties(guest_exception_handler PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sync_on_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyncOnAddress.cpp) target_include_directories(guest_sync_on_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sync_on_address_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 8e59a6b68..ba53dbdba 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -2,19 +2,39 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include + +namespace { + +constexpr std::array AllowedSignals{1, 4, 8, 10, 11, 30}; + +std::mutex handlersLock; +std::array handlers{}; + +bool Allowed(int signum) { + for (const int allowed : AllowedSignals) + if (allowed == signum) return true; + return false; +} + +} extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler) { - (void)signum; - (void)handler; - NotImplemented_nid_no_patch(__func__); + if (!Allowed(signum) || handler == nullptr) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(handlersLock); + if (handlers[signum] != nullptr) return SCE_KERNEL_ERROR_EAGAIN; + handlers[signum] = handler; return 0; } int APS5_VABI sceKernelRemoveExceptionHandler(int signum) { - (void)signum; - NotImplemented_nid_no_patch(__func__); + if (!Allowed(signum)) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(handlersLock); + handlers[signum] = nullptr; return 0; } diff --git a/core/libs/tests/GuestExceptionHandler.cpp b/core/libs/tests/GuestExceptionHandler.cpp new file mode 100644 index 000000000..fbb5dfd8f --- /dev/null +++ b/core/libs/tests/GuestExceptionHandler.cpp @@ -0,0 +1,33 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler); +int APS5_VABI sceKernelRemoveExceptionHandler(int signum); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EAGAIN = static_cast(0x80020023); + +static void APS5_VABI Handler(int, void*) {} +static void APS5_VABI Other(int, void*) {} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + auto* handler = reinterpret_cast(&Handler); + auto* other = reinterpret_cast(&Other); + for (const int rejected : {0, 2, 9, 31, 128, -1}) + Require(sceKernelInstallExceptionHandler(rejected, handler) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelInstallExceptionHandler(30, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelRemoveExceptionHandler(9) == SCE_KERNEL_ERROR_EINVAL); + for (const int signum : {1, 4, 8, 10, 11, 30}) { + Require(sceKernelInstallExceptionHandler(signum, handler) == 0); + Require(sceKernelInstallExceptionHandler(signum, other) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelRemoveExceptionHandler(signum) == 0); + Require(sceKernelInstallExceptionHandler(signum, other) == 0); + Require(sceKernelRemoveExceptionHandler(signum) == 0); + } + Require(sceKernelRemoveExceptionHandler(30) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 02ea84a84..9c0d96338 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -83,6 +83,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler but nothing delivers to it: a host fault still ends the process, and `sceKernelRaiseException` is not implemented. PPSA12544's PS5Util module (Unity) installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection, so delivery must run the handler on the target thread with a FreeBSD `ucontext` + - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. From 1d9d5ab491029389cb21dfd625f86569514255db Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 20:20:14 +0200 Subject: [PATCH 018/203] fix(libs): keep a $ORIGIN runpath in the patched prx libraries The NID patcher rebuilds .dynstr keeping only the DT_NEEDED and DT_SONAME strings, so DT_RUNPATH kept a stale offset and the patched libraries shipped with an empty runpath. A library could then load only those sibling prx that the executable already loads itself: libSceVideoOut.prx needs libSceKeyboard.prx, which a title that does not import libSceKeyboard never loads, so it fails at startup. The patcher now keeps DT_RPATH and DT_RUNPATH strings, and the prx libraries are linked with a $ORIGIN runpath instead of the build directory, so they find each other wherever they are copied. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 4 ++ core/libs/nid/include/nid/ElfPatcher.hpp | 6 +++ core/libs/nid/src/ElfNidPatcher.cpp | 4 +- core/libs/nid/tests/test_dynamic_strings.py | 42 +++++++++++++++++++++ core/libs/prx/libkernel/CMakeLists.txt | 6 +++ 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 core/libs/nid/tests/test_dynamic_strings.py diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 510110335..0212df94a 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -51,6 +51,8 @@ set_target_properties(libc PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} + BUILD_WITH_INSTALL_RPATH ON + INSTALL_RPATH "$ORIGIN" ) set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) @@ -101,6 +103,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} + BUILD_WITH_INSTALL_RPATH ON + INSTALL_RPATH "$ORIGIN" ) set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx) diff --git a/core/libs/nid/include/nid/ElfPatcher.hpp b/core/libs/nid/include/nid/ElfPatcher.hpp index dd70791c7..8dbc7d4bf 100644 --- a/core/libs/nid/include/nid/ElfPatcher.hpp +++ b/core/libs/nid/include/nid/ElfPatcher.hpp @@ -83,7 +83,13 @@ struct GnuHashLayout { constexpr std::int64_t kDtNeeded = 1; constexpr std::int64_t kDtSoname = 14; +constexpr std::int64_t kDtRpath = 15; +constexpr std::int64_t kDtRunpath = 29; constexpr std::int64_t kDtNull = 0; + +inline bool IsDynamicNameTag(std::int64_t tag) { + return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath; +} constexpr std::uint32_t kShtDynamic = 6u; constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu; constexpr std::uint32_t kShtDynsym = 11u; diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index aa9bd06c0..319cf1feb 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -190,7 +190,7 @@ void RemapDynamicNeededOffsets( for (std::size_t d = 0u; d < dynCount; ++d) { const std::size_t entryOffset = dynOffset + d * sizeof(Elf64_Dyn); const auto dyn = Read(elf, entryOffset); - if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue; + if (!IsDynamicNameTag(dyn.d_tag)) continue; const auto oldNameOffset = static_cast(dyn.d_val); const auto mapped = std::find_if( @@ -266,7 +266,7 @@ std::vector CollectDynamicNameOffsets(const std::vector(elf, dynOffset + d * sizeof(Elf64_Dyn)); if (dyn.d_tag == kDtNull) break; - if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue; + if (!IsDynamicNameTag(dyn.d_tag)) continue; offsets.push_back(static_cast(dyn.d_val)); } } diff --git a/core/libs/nid/tests/test_dynamic_strings.py b/core/libs/nid/tests/test_dynamic_strings.py new file mode 100644 index 000000000..1f9bbc847 --- /dev/null +++ b/core/libs/nid/tests/test_dynamic_strings.py @@ -0,0 +1,42 @@ +import re +import struct +import sys +from pathlib import Path + +DT_NULL = 0 +DT_NEEDED = 1 +DT_SONAME = 14 +DT_RPATH = 15 +DT_RUNPATH = 29 +SHT_DYNAMIC = 6 + + +def dynamic_strings(image): + shoff, = struct.unpack_from(" guest_module_fixture $ VERBATIM) add_dependencies(guest_module_fixture nid_patcher) + set_target_properties(guest_module_fixture PROPERTIES BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN") + find_package(Python3 QUIET COMPONENTS Interpreter) + if(Python3_Interpreter_FOUND AND NOT WIN32) + add_test(NAME nid_patcher_dynamic_strings COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/nid/tests/test_dynamic_strings.py $) + endif() add_test_executable(guest_dynamic_loader_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoader.cpp) target_include_directories(guest_dynamic_loader_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dynamic_loader_tests PRIVATE libkernel libc) From 12c229d19ac7f1e4faa345b6d82771e9ceb9cd82 Mon Sep 17 00:00:00 2001 From: RomainROCH Date: Sun, 4 Oct 2026 05:29:53 +0200 Subject: [PATCH 019/203] fix(jpegenc): honor chroma sampling and encode true grayscale JPEGs --- .gitmodules | 3 ++ 3rdparty/libjpeg-turbo | 1 + core/Decoder/Jpeg/CMakeLists.txt | 48 +++++++++++++++++- core/Decoder/Jpeg/include/Decoder/Jpeg.hpp | 8 ++- core/Decoder/Jpeg/src/Jpeg.cpp | 57 ++++++++++++++++------ core/Decoder/Jpeg/tests/Jpeg.cpp | 32 ++++++++++++ core/libs/prx/libSceJpegEnc/Export.cpp | 16 ++++-- core/libs/tests/GuestJpegEnc.cpp | 36 ++++++++++++-- docs/dev/TechnicalDebt.md | 1 - 9 files changed, 173 insertions(+), 29 deletions(-) create mode 160000 3rdparty/libjpeg-turbo diff --git a/.gitmodules b/.gitmodules index 0a5f83fb7..fc2e48631 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "3rdparty/stb"] path = 3rdparty/stb url = https://github.com/nothings/stb.git +[submodule "3rdparty/libjpeg-turbo"] + path = 3rdparty/libjpeg-turbo + url = https://github.com/libjpeg-turbo/libjpeg-turbo.git diff --git a/3rdparty/libjpeg-turbo b/3rdparty/libjpeg-turbo new file mode 160000 index 000000000..c85e6b905 --- /dev/null +++ b/3rdparty/libjpeg-turbo @@ -0,0 +1 @@ +Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344 diff --git a/core/Decoder/Jpeg/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt index 2280f398b..fcb338b8c 100644 --- a/core/Decoder/Jpeg/CMakeLists.txt +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -3,10 +3,56 @@ function(add_jpeg_decoder target runtimeTarget) message(FATAL_ERROR "JPEG decoder requires an existing runtime target: ${runtimeTarget}") endif() + include(ExternalProject) + if(NOT TARGET jpeg_turbo_external) + set(jpeg_turbo_prefix "${CMAKE_BINARY_DIR}/libjpeg-turbo") + if(WIN32) + if(MSVC) + set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/turbojpeg-static.lib") + else() + set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") + endif() + else() + set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") + endif() + file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include") + ExternalProject_Add(jpeg_turbo_external + SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo" + BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build" + INSTALL_DIR "${jpeg_turbo_prefix}" + CMAKE_ARGS + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_PREFIX=${jpeg_turbo_prefix} + -DBUILD_SHARED_LIBS=OFF + -DENABLE_STATIC=ON + -DENABLE_SHARED=OFF + -DWITH_SIMD=OFF + -DWITH_TURBOJPEG=ON + -DWITH_TOOLS=OFF + -DWITH_TESTS=OFF + -DCMAKE_INSTALL_LIBDIR=lib + -DCMAKE_C_VISIBILITY_PRESET=hidden + -DCMAKE_VISIBILITY_INLINES_HIDDEN=ON + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} + -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} + -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER} + BUILD_BYPRODUCTS + "${jpeg_turbo_library}" + ) + add_library(jpeg_turbo STATIC IMPORTED GLOBAL) + set_target_properties(jpeg_turbo PROPERTIES + IMPORTED_LOCATION "${jpeg_turbo_library}" + INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo/src;${jpeg_turbo_prefix}/include" + ) + add_dependencies(jpeg_turbo jpeg_turbo_external) + endif() + add_library(${target} STATIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/src/Jpeg.cpp) target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include) target_include_directories(${target} SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/stb) - target_link_libraries(${target} PRIVATE ${runtimeTarget}) + target_link_libraries(${target} PRIVATE ${runtimeTarget} jpeg_turbo) set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF CXX_VISIBILITY_PRESET hidden diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 275217fb2..60142fc36 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -21,8 +21,14 @@ struct Header { std::uint32_t channels; }; +enum class Sampling { + Yuv444, + Yuv422, + Yuv420, +}; + std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality); + std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444); std::optional
ParseHeader(std::span jpeg); diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index 74a50c4f8..c8f1e1eca 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -4,16 +4,12 @@ #include #include -#define STB_IMAGE_WRITE_STATIC -#define STB_IMAGE_WRITE_IMPLEMENTATION -#define STBI_WRITE_NO_STDIO -#include "stb_image_write.h" - #define STB_IMAGE_STATIC #define STB_IMAGE_IMPLEMENTATION #define STBI_ONLY_JPEG #define STBI_NO_STDIO #include "stb_image.h" +#include "turbojpeg.h" namespace Decoder::Jpeg { @@ -21,16 +17,39 @@ namespace { constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; -void appendBytes(void* context, void* data, int size) { - auto* output = static_cast*>(context); - const auto* bytes = static_cast(data); - output->insert(output->end(), bytes, bytes + size); +class Handle { +public: + Handle() : handle(tj3Init(TJINIT_COMPRESS)) { + if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed"); + } + ~Handle() { tj3Destroy(handle); } + Handle(const Handle&) = delete; + Handle& operator=(const Handle&) = delete; + operator tjhandle() const { return handle; } + +private: + tjhandle handle; +}; + +class Buffer { +public: + ~Buffer() { tj3Free(data); } + unsigned char* data = nullptr; +}; + +int toSubsampling(Sampling sampling) { + switch (sampling) { + case Sampling::Yuv444: return TJSAMP_444; + case Sampling::Yuv422: return TJSAMP_422; + case Sampling::Yuv420: return TJSAMP_420; + } + throw std::invalid_argument("Jpeg::Encode: invalid sampling"); } } // namespace std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality) { + std::uint32_t channels, int quality, Sampling sampling) { if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { throw std::invalid_argument("Jpeg::Encode: unsupported image size"); } @@ -39,12 +58,18 @@ std::vector Encode(std::span pixels, std::uint if (pixels.size() < static_cast(width) * height * channels) { throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); } - - std::vector output; - const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast(width), static_cast(height), - static_cast(channels), pixels.data(), quality); - if (written == 0) throw std::runtime_error("Jpeg::Encode: encoding failed"); - return output; + const int subsampling = toSubsampling(sampling); + Handle handle; + if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { + throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); + } + Buffer buffer; + std::size_t size = 0; + const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB; + if (tj3Compress8(handle, pixels.data(), static_cast(width), 0, static_cast(height), pixelFormat, &buffer.data, &size) < 0) { + throw std::runtime_error("Jpeg::Encode: encoding failed"); + } + return {buffer.data, buffer.data + size}; } std::optional
ParseHeader(std::span jpeg) { diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index 38e2aa42d..d396a00f2 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } @@ -25,6 +26,25 @@ static int Difference(std::uint8_t left, std::uint8_t right) { return left > right ? left - right : right - left; } +struct FrameInfo { int components; std::uint8_t sampling; }; + +static FrameInfo ReadFrame(const std::vector& jpeg) { + std::size_t offset = 2; + while (offset + 3 < jpeg.size()) { + Require(jpeg[offset++] == 0xFF); + while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset; + Require(offset < jpeg.size()); + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xDA || marker == 0xD9) break; + Require(offset + 1 < jpeg.size()); + const std::size_t length = (static_cast(jpeg[offset]) << 8) | jpeg[offset + 1]; + Require(length >= 2 && offset + length <= jpeg.size()); + if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]}; + offset += length; + } + std::abort(); +} + int main() { constexpr std::uint32_t width = 32; constexpr std::uint32_t height = 24; @@ -41,6 +61,7 @@ int main() { const std::vector rgbJpeg = Decoder::Jpeg::Encode(rgb, width, height, 3, 90); Require(IsJpeg(rgbJpeg)); + Require(ReadFrame(rgbJpeg).components == 3 && ReadFrame(rgbJpeg).sampling == 0x11); const auto rgbImage = Decoder::Jpeg::Decode(rgbJpeg); Require(rgbImage.has_value()); Require(rgbImage->width == width && rgbImage->height == height && rgbImage->channels == 3); @@ -55,6 +76,7 @@ int main() { } const std::vector grayJpeg = Decoder::Jpeg::Encode(gray, width, height, 1, 90); Require(IsJpeg(grayJpeg)); + Require(ReadFrame(grayJpeg).components == 1); const auto grayImage = Decoder::Jpeg::Decode(grayJpeg); Require(grayImage.has_value()); Require(grayImage->width == width && grayImage->height == height); @@ -69,6 +91,16 @@ int main() { Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, 0x10000, 1, 3, 90); })); Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height + 1, 3, 90); })); + for (const auto [sampling, factor] : {std::pair{Decoder::Jpeg::Sampling::Yuv444, std::uint8_t{0x11}}, + std::pair{Decoder::Jpeg::Sampling::Yuv422, std::uint8_t{0x21}}, + std::pair{Decoder::Jpeg::Sampling::Yuv420, std::uint8_t{0x22}}}) { + const auto encoded = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 1, sampling); + Require(ReadFrame(encoded).components == 3 && ReadFrame(encoded).sampling == factor); + const auto highQuality = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 100, sampling); + Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor); + } + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast(3)); })); + Require(!Decoder::Jpeg::Decode({}).has_value()); const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; Require(!Decoder::Jpeg::Decode(garbage).has_value()); diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index d52ad86ac..2d4c66657 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -41,7 +41,6 @@ constexpr std::uint8_t SAMPLING_TYPE_420 = 2; constexpr std::uint32_t MAX_IMAGE_DIMENSION = 0xFFFF; constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF; constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF; -constexpr int SUBSAMPLED_MAX_QUALITY = 90; struct Encoder { Encoder* self; @@ -148,9 +147,16 @@ std::vector toPackedPixels(const JpegEncEncodeParam& param, std::u } int toQuality(const JpegEncEncodeParam& param) { - int quality = 100 - param.compression_ratio * 99 / 255; - if (param.sampling_type != SAMPLING_TYPE_FULL) quality = std::min(quality, SUBSAMPLED_MAX_QUALITY); - return quality; + return 100 - param.compression_ratio * 99 / 255; +} + +Decoder::Jpeg::Sampling toSampling(const JpegEncEncodeParam& param) { + switch (param.sampling_type) { + case SAMPLING_TYPE_FULL: return Decoder::Jpeg::Sampling::Yuv444; + case SAMPLING_TYPE_422: return Decoder::Jpeg::Sampling::Yuv422; + case SAMPLING_TYPE_420: return Decoder::Jpeg::Sampling::Yuv420; + } + throw std::invalid_argument("sceJpegEncEncode: invalid sampling type"); } } // namespace @@ -187,7 +193,7 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param const std::uint32_t channels = param->pixel_format == PIXEL_FORMAT_Y8 ? 1 : 3; const std::vector pixels = toPackedPixels(*param, channels); - const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param)); + const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param)); if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE; std::memcpy(param->jpeg, jpeg.data(), jpeg.size()); diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 7f5983747..5a49caa48 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -23,17 +24,34 @@ static bool IsJpeg(const unsigned char* data, std::uint32_t size) { return size > 4 && data[0] == 0xFF && data[1] == 0xD8 && data[size - 2] == 0xFF && data[size - 1] == 0xD9; } +static std::pair ReadFrame(const unsigned char* jpeg, std::uint32_t size) { + std::size_t offset = 2; + while (offset + 3 < size) { + Require(jpeg[offset++] == 0xFF); + while (offset < size && jpeg[offset] == 0xFF) ++offset; + Require(offset < size); + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xDA || marker == 0xD9) break; + Require(offset + 1 < size); + const std::size_t length = (static_cast(jpeg[offset]) << 8) | jpeg[offset + 1]; + Require(length >= 2 && offset + length <= size); + if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]}; + offset += length; + } + std::abort(); +} + static int AverageError(const std::vector& expected, const std::vector& actual) { long total = 0; for (std::size_t i = 0; i < expected.size(); ++i) total += expected[i] > actual[i] ? expected[i] - actual[i] : actual[i] - expected[i]; return static_cast(total / static_cast(expected.size())); } -static std::vector DecodeOutput(const JpegEncOutputInfo& info) { +static std::vector DecodeOutput(const JpegEncOutputInfo& info, std::uint32_t channels = 3) { Require(IsJpeg(jpeg, info.size)); const auto decoded = Decoder::Jpeg::Decode({jpeg, info.size}); Require(decoded.has_value()); - Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == 3); + Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == channels); return decoded->pixels; } @@ -169,6 +187,7 @@ int main() { info = {}; Require(sceJpegEncEncode(handle, &rgba, &info) == 0); Require(info.height == 16); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); for (std::uint32_t y = 0; y < 16; ++y) { @@ -183,6 +202,7 @@ int main() { bgra.pixel_format = 1; info = {}; Require(sceJpegEncEncode(handle, &bgra, &info) == 0); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); alignas(4) static unsigned char yuyv[16 * 16 * 2]; @@ -202,10 +222,8 @@ int main() { Require(AverageError(std::vector(16 * 16 * 3, 100), DecodeOutput(info)) < 3); static unsigned char gray[16 * 16]; - std::vector expectedGray(16 * 16 * 3); for (std::size_t i = 0; i < sizeof(gray); ++i) { gray[i] = static_cast(((i % 16) + (i / 16)) * 8); - for (int c = 0; c < 3; ++c) expectedGray[i * 3 + c] = gray[i]; } JpegEncEncodeParam y8 = ValidEncodeParam(); y8.image = gray; @@ -216,7 +234,15 @@ int main() { y8.sampling_type = 0; info = {}; Require(sceJpegEncEncode(handle, &y8, &info) == 0); - Require(AverageError(expectedGray, DecodeOutput(info)) < 6); + Require(ReadFrame(jpeg, info.size) == std::pair{1, std::uint8_t{0x11}}); + const auto decodedGray = DecodeOutput(info, 1); + Require(AverageError(std::vector(gray, gray + sizeof(gray)), decodedGray) < 6); + + rgba.sampling_type = 1; + rgba.compression_ratio = 0; + info = {}; + Require(sceJpegEncEncode(handle, &rgba, &info) == 0); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x21}}); std::memset(jpeg, 0, sizeof(jpeg)); Require(sceJpegEncEncode(handle, &rgba, nullptr) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2ddee4350..14b8e47df 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -109,7 +109,6 @@ Throughout the project, every function at every stage either **does exactly what - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted, a positive denormal src2 counts as positive): the recompiler does not read MODE, so other IEEE/denormal settings are not modelled. - [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. -- [libSceJpegEnc](../../core/libs/prx/libSceJpegEnc/Export.cpp) encodes 4:2:2 sampling requests with 4:2:0 chroma subsampling, and grayscale requests as a 3-component JPEG with neutral chroma instead of a single-component one: the [JPEG encoder](../../core/Decoder/Jpeg/src/Jpeg.cpp) (stb) only produces 3-component 4:2:0 and 4:4:4 images. - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink. From d4eed90b54caf5a0fde33abfbb7caf3027732f80 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:12:03 +0200 Subject: [PATCH 020/203] fix(videoout): report unavailable output modes instead of throwing sceVideoOutIsOutputSupported threw VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE for every mode other than the default and 119.88 Hz, and sceVideoOutConfigureOutput threw for an unavailable 119.88 Hz mode. Titles probe and request modes the display may lack and handle the result: PPSA12544 asks for 0xd000000a and falls back to 3840 pixels when it is not supported, and its sceVideoOutConfigureOutput caller switches on the returned error codes, including VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE. Every mode except the default now reports 0, and configuring one returns VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/src/Output.cpp | 9 +++------ core/libs/prx/libSceVideoOut/tests/Flip.cpp | 7 +++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index c595e3022..72f82ba16 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -25,9 +25,6 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO } } } - if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE"); - } return 0; } @@ -190,7 +187,7 @@ int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const Vide if (result != 0) { return result; } - return (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? 0 : 1; + return mode == VIDEO_OUT_OUTPUT_MODE_DEFAULT ? 1 : 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } @@ -200,8 +197,8 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO if (supported < 0) { return supported; } - if (supported == 0 && mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE"); + if (supported == 0) { + return VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE; } auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 9dd1463b0..42a509b76 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -208,6 +209,12 @@ void testControls() { const auto handle = sceVideoOutOpen(255, 0, 0, openParam.data()); check(handle >= 0, "open with the highest priority on every CPU failed"); const auto cfg = VideoOutDriver::Get().GetConfig(handle); + check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported"); + for (const uint64_t unsupported : std::initializer_list{VIDEO_OUT_OUTPUT_MODE_119_88HZ, 0xd000000aull}) { + check(sceVideoOutIsOutputSupported(handle, unsupported, nullptr, nullptr, 0) == 0, "unavailable output mode is supported"); + check(sceVideoOutConfigureOutput(handle, unsupported, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE, "unavailable output mode was configured"); + } + check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 0, "default output mode was rejected"); for (int rate = 0; rate <= 2; ++rate) { check(sceVideoOutSetFlipRate(handle, rate) == 0 && cfg->flipRate == rate, "flip rate was not applied"); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c457ddd14..a13771a7a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,6 +29,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted +- [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - output mode encoding unknown: every mode except the default reports not supported (0), and `sceVideoOutConfigureOutput` returns `VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE` for it, instead of returning `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` for values the console does not define. PPSA12544 probes `0xd000000a` and selects 7680 pixels when it is supported, 3840 otherwise - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order From d82bae0934111ea1568f53730412c7df739bc030 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 23:09:27 +0200 Subject: [PATCH 021/203] feat(libs): add exports imported by PPSA12544's Unity plugins PPSA12544's Media/Plugins modules (CommonDialog, PSN, Share) import libSceVrSetupDialog, which no library provided, and Share, NpTrophy2, WebBrowserDialog and NpEntitlementAccess functions that were not exported. Add libSceVrSetupDialog and the missing functions as unimplemented exports, except sceNpEntitlementAccessRequestConsumeServiceEntitlement, which reports SCE_NP_ERROR_SIGNED_OUT like sceNpEntitlementAccessRequestConsumeUnifiedEntitlement. Their other imports are covered by #734 and #369, and two by #407. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceNpEntitlementAccess/Export.cpp | 26 +++++++++++++ core/libs/prx/libSceNpTrophy2/src/Context.cpp | 11 ++++++ core/libs/prx/libSceShare/Export.cpp | 16 ++++++++ core/libs/prx/libSceVrSetupDialog/Export.cpp | 38 +++++++++++++++++++ .../prx/libSceWebBrowserDialog/Export.cpp | 6 +++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 98 insertions(+) create mode 100644 core/libs/prx/libSceVrSetupDialog/Export.cpp diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 05fc69362..31fa46049 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -118,4 +118,30 @@ int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(void) { return SCE_NP_ERROR_SIGNED_OUT; } + +int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { + return SCE_NP_ERROR_SIGNED_OUT; +} + + +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index e259afb73..0a7bd1daf 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -34,4 +34,15 @@ int APS5_VABI sceNpTrophy2UnregisterUnlockCallback() { return SCE_NP_TROPHY2_OK; } + +int APS5_VABI sceNpTrophy2GetRewardIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2ShowTrophyList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index caabed9b9..ffebda5e9 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -100,4 +100,20 @@ int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, int3 return ERROR_NOT_SUPPORTED; } + +int APS5_VABI sceShareCaptureVideoClipExtended(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareGetRunningStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareSetContentParamForApplicationTitle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp new file mode 100644 index 000000000..5c9a5da6a --- /dev/null +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -0,0 +1,38 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVrSetupDialogClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogUpdateStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 879608486..af2a92791 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -59,4 +59,10 @@ int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { return g_status.load(); } + +int APS5_VABI sceWebBrowserDialogSetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c457ddd14..a69fe179d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -70,6 +70,7 @@ Throughout the project, every function at every stage either **does exactly what - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures +- [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp), and the Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation, have unknown signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap From d4b01b72c032695021c2014bb4916a3a6bf8a031 Mon Sep 17 00:00:00 2001 From: Chad Date: Mon, 5 Oct 2026 20:11:20 -0500 Subject: [PATCH 022/203] feat(libkernel): accept PTHREAD_PROCESS_SHARED barrier attributes The PS5 kernel follows FreeBSD 11, whose libthr accepts process-shared barriers. Guest code runs in a single process, so a process-shared barrier synchronises the same threads a private one does; the attribute is stored and reported back by getpshared. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp | 3 +-- core/libs/tests/GuestPthreadBarrier.cpp | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 1c363ef32..140b6900e 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -54,8 +54,7 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPrivate** attr, int pshared) { if (!attr || !*attr) return 22; - if (pshared == PROCESS_SHARED) NotImplemented_nid_no_patch("pthread_barrierattr_setpshared: PTHREAD_PROCESS_SHARED"); - if (pshared != PROCESS_PRIVATE) return 22; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return 22; (*attr)->pshared = pshared; return 0; } diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index d7286b7ee..64d67e201 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -66,14 +66,16 @@ int main() { Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, 2) == GUEST_EINVAL); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, -1) == GUEST_EINVAL); - bool rejected = false; - try { pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == 0); + Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_SHARED); + Context shared; + Require(pthread_barrier_init_nid_postfix(&shared.barrier, &attr, WAITERS) == 0); + RunWaiters(shared); + Require(pthread_barrier_destroy_nid_postfix(&shared.barrier) == 0); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_PRIVATE) == 0); pshared = -1; Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); - rejected = false; + bool rejected = false; try { pthread_barrierattr_getpshared_nid_postfix(&attr, nullptr); } catch (const std::invalid_argument&) { rejected = true; } Require(rejected); From 5a321471c20d619d83ba4d3c46935229eafb1c8a Mon Sep 17 00:00:00 2001 From: Chad Date: Mon, 5 Oct 2026 20:16:49 -0500 Subject: [PATCH 023/203] feat(libSceHttp2): implement cookie boxes and requests without a content length sceHttp2CreateCookieBox hands out an id from the library's handle counter, sceHttp2SetCookieBox binds one to a template or request, and sceHttp2CookieFlush succeeds: no request reaches the network, so no cookie is ever stored. sceHttp2SetRequestNoContentLength is accepted like sceHttp2SetRequestContentLength. The library gets its own CMakeLists for the new guest_http2 test. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHttp2/CMakeLists.txt | 21 ++++++++++++++ core/libs/prx/libSceHttp2/Export.cpp | 23 +++++++-------- core/libs/tests/GuestHttp2.cpp | 36 ++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceHttp2/CMakeLists.txt create mode 100644 core/libs/tests/GuestHttp2.cpp diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt new file mode 100644 index 000000000..08d9a13c8 --- /dev/null +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceHttp2 PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceHttp2) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_http2_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp2.cpp) + target_include_directories(guest_http2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libc) + configure_windows_unwind(guest_http2_tests) + add_test(NAME guest_http2 COMMAND guest_http2_tests) + if(WIN32) + set_tests_properties(guest_http2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index adaa00e78..a90ecb83f 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -214,23 +214,24 @@ int APS5_VABI sceHttp2GetMemoryPoolStats() { return 0; } -int APS5_VABI sceHttp2CookieFlush(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttp2CookieFlush(int id) { + (void)id; return 0; } -int APS5_VABI sceHttp2CreateCookieBox(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { + (void)lib_http2_ctx_id; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { + (void)id; + (void)cookie_box_id; return 0; } -int APS5_VABI sceHttp2SetCookieBox(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttp2SetRequestNoContentLength(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttp2SetRequestNoContentLength(int id) { + (void)id; return 0; } diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp new file mode 100644 index 000000000..7be4800b5 --- /dev/null +++ b/core/libs/tests/GuestHttp2.cpp @@ -0,0 +1,36 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceHttp2Init(int, int, std::size_t, int); +int APS5_VABI sceHttp2CreateTemplate(int, const char*, int, int); +int APS5_VABI sceHttp2CreateRequestWithURL(int, const char*, const char*, std::uint64_t); +int APS5_VABI sceHttp2CreateCookieBox(int); +int APS5_VABI sceHttp2SetCookieBox(int, int); +int APS5_VABI sceHttp2CookieFlush(int); +int APS5_VABI sceHttp2SetRequestNoContentLength(int); +int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t); +int APS5_VABI sceHttp2Term(int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const int context = sceHttp2Init(1, 1, 0x10000, 4); + Require(context > 0); + const int box = sceHttp2CreateCookieBox(context); + const int other = sceHttp2CreateCookieBox(context); + Require(box > 0 && other > 0 && box != other && box != context); + const int tmpl = sceHttp2CreateTemplate(context, "agent", 2, 0); + Require(tmpl > 0 && tmpl != box && tmpl != other); + Require(sceHttp2SetCookieBox(tmpl, box) == 0); + Require(sceHttp2SetCookieBox(tmpl, 0) == 0); + const int request = sceHttp2CreateRequestWithURL(tmpl, "POST", "https://example.com/", 16); + Require(request > 0); + Require(sceHttp2SetCookieBox(request, other) == 0); + Require(sceHttp2SetRequestNoContentLength(request) == 0); + Require(sceHttp2CookieFlush(context) == 0); + Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); + Require(sceHttp2Term(context) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b57c3f774..74f3cd5a0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -117,6 +117,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown +- [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard ### Functional From bf0ee4019de11213ead9d80d50cc76dcd839ad8f Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:11:04 -0300 Subject: [PATCH 024/203] fix(relinker): bind Windows imports to their declared modules --- core/relinker/CMakeLists.txt | 2 + .../domain/include/domain/GuestRuntime.hpp | 2 + .../domain/include/domain/ImportModule.hpp | 35 +++++ core/relinker/domain/include/domain/Types.hpp | 1 + .../elfpatcher/windows/WindowsPeFormat.hpp | 1 + .../src/windows/WindowsEntryStubBuilder.cpp | 42 +++++- .../src/windows/WindowsGuestModuleWriter.cpp | 6 +- .../src/windows/WindowsPePatcher.cpp | 2 +- .../src/windows/WindowsRelocationBuilder.cpp | 2 + .../include/relinker/guest/GuestImage.hpp | 1 + .../relinker/src/guest/GuestImageReader.cpp | 9 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 30 +++-- .../src/output/SysVDynamicSectionBuilder.cpp | 1 + .../src/pipeline/RelinkerPipeline.cpp | 10 +- .../tests/test_guest_module_directories.py | 11 +- .../tests/test_windows_import_modules.py | 123 ++++++++++++++++++ 16 files changed, 249 insertions(+), 29 deletions(-) create mode 100644 core/relinker/domain/include/domain/ImportModule.hpp create mode 100644 core/relinker/relinker/tests/test_windows_import_modules.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 006738d94..bc1184b2c 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -224,6 +224,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) + add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index a90225b54..2c45abb92 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -12,6 +12,7 @@ struct GuestImport { std::uint32_t TargetRva; std::uint64_t Addend; std::uint32_t RelocationType = 1; + std::string Library; }; struct GuestRuntime { @@ -22,6 +23,7 @@ struct GuestRuntime { std::uint32_t InitRva = 0; std::uint32_t FiniRva = 0; bool UsePlatformTlsResolver = true; + std::vector Names; }; } diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp new file mode 100644 index 000000000..fb78c079a --- /dev/null +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -0,0 +1,35 @@ +#ifndef DOMAIN_IMPORTMODULE_HPP +#define DOMAIN_IMPORTMODULE_HPP + +#include +#include + +namespace Domain { + +inline std::string ImportModule(const std::string& symbol, const std::map& modules) { + if (modules.empty()) return {}; + const auto first = symbol.find('#'); + if (first == std::string::npos) return {}; + const auto second = symbol.find('#', first + 1); + if (second == std::string::npos || second + 1 == symbol.size()) + throw RelinkerException("Invalid qualified import: " + symbol); + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"; + std::uint64_t id = 0; + for (const char character : std::string_view(symbol).substr(second + 1)) { + const auto digit = alphabet.find(character); + if (digit == std::string_view::npos || id > (65535u - digit) / 64u) + throw RelinkerException("Invalid import module ID: " + symbol); + id = id * 64u + digit; + } + const auto module = modules.find(id); + if (module == modules.end()) throw RelinkerException("Unknown import module ID: " + symbol); + auto name = module->second; + if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) + throw RelinkerException("Invalid import module name: " + name); + if (!name.ends_with(".prx")) name += ".prx"; + return name; +} + +} + +#endif diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index d9caa6a41..e49cc0347 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -95,6 +95,7 @@ struct SysVDynamicSection { std::vector RelaData; std::vector RelaPltData; std::vector GuestModules; + std::map ImportModules; }; struct CallRegistryEntry { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index bc1c0984d..bef348f4a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -34,6 +34,7 @@ struct PeImport { std::uint64_t Addend; std::int32_t TargetModule = -1; std::uint32_t RelocationType = 1; + std::string Library; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 13b4caeb3..230d805a8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Elfpatcher::Windows { @@ -85,6 +86,35 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con for (const auto& import : imports) symbolNames.push_back(addString(import.Name)); + std::vector searchTables; + std::vector searchCounts; + std::vector platformTlsResolvers; + for (const auto& import : imports) { + std::vector order; + for (std::size_t index = 0; index < libraries.size(); ++index) { + auto name = std::filesystem::path(libraries[index]).filename().string(); + if (name.ends_with(".guest.prx")) name.resize(name.size() - 10); + const bool guestName = index < guestModules.size() && + std::find(guestModules[index].Names.begin(), guestModules[index].Names.end(), import.Library) != guestModules[index].Names.end(); + if (import.Library.empty() || name == import.Library || guestName) order.push_back(CheckedRva(index)); + } + if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library); + const bool hostModule = !import.Library.empty() && + std::any_of(order.begin(), order.end(), [&](auto index) { return index >= guestModules.size(); }); + if (hostModule) { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libc.prx" && import.Library != "libc.prx") order.push_back(CheckedRva(index)); + } + } + Io::AlignBuffer(data, 4); + platformTlsResolvers.push_back(std::none_of(order.begin(), order.end(), [&](auto index) { + return index < guestModules.size() && !guestModules[index].UsePlatformTlsResolver; + })); + searchTables.push_back(CheckedRva(dataRva + data.size())); + searchCounts.push_back(CheckedRva(order.size())); + for (const auto index : order) Io::AppendU32(data, index); + } + const auto lastError = reserve(4); const auto errorDigits = reserve(11); const auto errorMessage = reserve(ErrorMessageCapacity); @@ -105,7 +135,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto diagnosticsOffset = data.size(); std::vector errors = {"FAIL: cannot obtain executable path\n", "FAIL: executable or library path is too long\n", "FAIL: executable path has no directory\n"}; for (const auto& import : imports) - errors.push_back("FAIL: unresolved ELF import " + import.Name + "\n"); + errors.push_back("FAIL: unresolved ELF import " + import.Name + + (import.Library.empty() ? std::string{} : " from " + import.Library) + "\n"); const auto argumentError = errors.size(); errors.push_back("FAIL: cannot prepare command-line arguments\n"); std::vector errorRvas; @@ -291,7 +322,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector lazyUnresolvedImports; std::vector tlsResolverAddresses; for (std::size_t index = 0; index < imports.size(); ++index) { - if (!guestModules.empty() && guestModules.front().UsePlatformTlsResolver && imports[index].Name == "vNe1w4diLCs") { + if (!guestModules.empty() && platformTlsResolvers[index] && imports[index].Name == "vNe1w4diLCs") { if (imports[index].Addend != 0) throw Domain::RelinkerException("TLS resolver import has an addend"); tlsResolverAddresses.push_back(code.Branch({0x48, 0x8d, 0x05})); guestStartup.WriteImport(code, imports[index], handles); @@ -299,14 +330,15 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } code.Rip({0x48, 0x8d, 0x1d}, handles); code.Rip({0x48, 0x8d, 0x35}, symbolNames[index]); + code.Rip({0x48, 0x8d, 0x3d}, searchTables[index]); code.Emit({0xbd}); - code.U32(CheckedRva(libraries.size())); + code.U32(searchCounts[index]); const auto search = code.GetRva(); - code.Emit({0x48, 0x8b, 0x0b, 0x48, 0x89, 0xf2}); + code.Emit({0x8b, 0x0f, 0x48, 0x8b, 0x0c, 0xcb, 0x48, 0x89, 0xf2}); call("GetProcAddress"); code.Emit({0x48, 0x85, 0xc0}); const auto resolved = code.Branch({0x0f, 0x85}); - code.Emit({0x48, 0x83, 0xc3, 8, 0xff, 0xcd}); + code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); if (lazyBinding) { lazyUnresolvedImports.push_back(index); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 5cdb3ed73..2dc27774d 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -47,13 +47,13 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI if (addend != 0 || (symbolIndex != 0 && (symbol.Info & 15) != 6)) throw Domain::RelinkerException("Invalid guest TLS module relocation", target); if (symbolIndex != 0 && symbol.Section == 0) { image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, 0, 16}); + runtime.Imports.push_back({symbol.Name, rva, 0, 16, symbol.Library}); } else tlsModules.emplace_back(target, rva); } else if (type == 17) { if (symbolIndex != 0 && (symbol.Info & 15) != 6) throw Domain::RelinkerException("Invalid guest TLS offset relocation", target); if (symbolIndex != 0 && symbol.Section == 0) { image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend, 17}); + runtime.Imports.push_back({symbol.Name, rva, addend, 17, symbol.Library}); } else image.WritePointer(target, symbol.Value + addend); } else if (type == 18) { const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); @@ -70,7 +70,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else { if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend}); + runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library}); } } else throw Domain::RelinkerException("Unsupported Windows guest relocation " + std::to_string(type), target); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 1b9370ceb..322fbd46c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -83,7 +83,7 @@ std::vector WindowsPePatcher::Patch(const std::vector(index), import.RelocationType}); + for (const auto& import : module.Imports) relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast(index), import.RelocationType, import.Library}); } libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); if (dependencyDiagnostics) diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index a2d33a623..b1f71e3f3 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -45,6 +45,8 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom image.RequireWritable(target, 8); image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); + if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) + result.Imports.back().Library = module->second; } }; applyTable(dynamicSection.RelaData, false); diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 572234514..fe34092b5 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -21,6 +21,7 @@ struct GuestSymbol { std::uint16_t Section; std::uint64_t Value; std::uint64_t Size; + std::string Library; }; struct GuestImage { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 837273006..eb9c6653d 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace Relinker { @@ -61,12 +62,14 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (loads.empty() || dynamic == nullptr || dynamic->FileSize % 16 != 0) fail("Missing or invalid ELF load/dynamic segments"); std::map tags; std::vector needed; + std::vector moduleImports; bool terminated = false; for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) { const auto tag = Io::ReadU64(bytes, offset); const auto value = Io::ReadU64(bytes, offset + 8); if (tag == 0) { terminated = true; break; } if (tag == 1) needed.push_back(value); + else if (tag == 0x61000045) moduleImports.push_back(value); else if (tag < 0x60000000 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); } @@ -118,9 +121,13 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto symOffset = table(6, 0x61000039, symSize); image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); image.Dynamic.DynStrData.push_back(0); + std::map importModules; + for (const auto value : moduleImports) + if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); std::set exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); + const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules) : std::string{}; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; @@ -144,7 +151,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); - image.Symbols.push_back({name, info, visibility, section, value, size}); + image.Symbols.push_back({name, info, visibility, section, value, size, library}); } if (tags.contains(14)) { image.Soname = string(tags.at(14)); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index fe15abdd9..9010692d3 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -49,7 +49,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; - std::map exports; + std::map> exports; std::map> sharedExports; std::set outputNames; Io::FileReader reader; @@ -66,12 +66,9 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; - const auto [existing, inserted] = exports.emplace(symbol.Name, images.size()); - if (inserted) continue; - if (windows || existing->second == images.size()) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + images.at(existing->second).SourcePath.string() + " and " + path.string()); - auto& providers = sharedExports[symbol.Name]; - providers.insert(existing->second); - providers.insert(images.size()); + auto& providers = exports[symbol.Name]; + if (std::find(providers.begin(), providers.end(), images.size()) == providers.end()) providers.push_back(images.size()); + if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end()); } std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); @@ -119,7 +116,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && exports.contains(name)) rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name); } std::vector> dependencies(images.size()); - for (auto& image : images) image.UsePlatformTlsResolver = !exports.contains("vNe1w4diLCs"); + for (auto& image : images) { + image.UsePlatformTlsResolver = std::none_of(image.Symbols.begin(), image.Symbols.end(), [](const auto& symbol) { + return symbol.Name == "vNe1w4diLCs" && symbol.Section != 0 && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2; + }); + } for (std::size_t index = 0; index < images.size(); ++index) { for (const auto& name : images[index].Dependencies) { const auto found = guestNames.find(name); @@ -129,11 +130,19 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (symbol.Section != 0 || symbol.Name.empty()) continue; rejectSharedImport(symbol.Name, images[index].SourcePath.string()); const auto found = exports.find(symbol.Name); + std::vector providers; if (found != exports.end()) { - const auto& provider = images[found->second]; + for (const auto provider : found->second) { + const auto& candidate = images[provider]; + if (!windows || symbol.Library.empty() || symbol.Library == candidate.SourcePath.filename().string() || symbol.Library == candidate.Soname) providers.push_back(provider); + } + } + if (providers.size() > 1) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); + if (!providers.empty()) { + const auto& provider = images[providers.front()]; const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Section != AbsoluteSection && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; }); if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (found->second != index) dependencies[index].insert(found->second); + if (providers.front() != index) dependencies[index].insert(providers.front()); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); } } @@ -202,6 +211,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Domain::GuestRuntime runtime; runtime.UsePlatformTlsResolver = image.UsePlatformTlsResolver; runtime.Path = relativeDirectory + "/" + image.OutputName; + runtime.Names = {image.SourcePath.filename().string(), image.Soname}; std::vector output; if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); else { diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 3e29923b9..991bb6ed3 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -85,6 +85,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( std::vector nonPltRefs; for (const auto& ref : nidReferences) { + if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2438ce6b3..90f154767 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker { @@ -175,6 +176,12 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (policy) policy->RegisterLibraryImport(snd); } + std::map importModules; + for (const auto& tag : dynTags) { + if (tag.Tag == 0x61000045 && !importModules.emplace(tag.Value >> 48, readCStr(tag.Value & 0xffffffffu)).second) + throw RelinkerException("Duplicate import module ID"); + } + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; off + relaEntSize <= relaSize; off += relaEntSize) { const FileByteOffset pos = relaOff + off; @@ -203,7 +210,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::uint32_t nameOff = 0; std::memcpy(&nameOff, raw.data() + symOff, 4); - nidRefs.push_back({readCStr(nameOff), {}, relType, pos, rOffset, rAddend}); + const auto name = readCStr(nameOff); + nidRefs.push_back({name, Domain::ImportModule(name, importModules), relType, pos, rOffset, rAddend}); } }; diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index 992f89abf..e1a1c0aae 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -122,12 +122,8 @@ def main(): (case / name).mkdir(parents=True) (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) result, output = convert(case, windows) - if windows: - assert result.returncode == 2 and "Duplicate guest export" in result.stderr, result.stderr - assert not output.exists() and not (case / "app0").exists(), output - else: - assert result.returncode == 0 and output.exists(), result.stderr - assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"sce_module.prx.guest.prx", "prx.prx.guest.prx"} + assert result.returncode == 0 and output.exists(), result.stderr + assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"sce_module.prx.guest.prx", "prx.prx.guest.prx"} case = work / f"{windows}-ambiguous-import" for name in ("sce_module", "prx"): @@ -135,8 +131,7 @@ def main(): (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) (case / "prx" / "consumer.prx").write_bytes(module_with_symbol(False)) result, output = convert(case, windows) - expected = "Duplicate guest export" if windows else "Ambiguous guest import shared" - assert result.returncode == 2 and expected in result.stderr, result.stderr + assert result.returncode == 2 and "Ambiguous guest import shared" in result.stderr, result.stderr assert not output.exists() and not (case / "app0").exists(), output print("Guest module directory integration tests passed") diff --git a/core/relinker/relinker/tests/test_windows_import_modules.py b/core/relinker/relinker/tests/test_windows_import_modules.py new file mode 100644 index 000000000..a83858e5c --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_import_modules.py @@ -0,0 +1,123 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol + + +def dynamic(image, offset, phoff, tags): + for index, tag in enumerate(tags + [(0, 0)]): + struct.pack_into(' Date: Mon, 5 Oct 2026 22:47:35 -0300 Subject: [PATCH 025/203] fix(relinker): map module name aliases to declared dependencies --- .../domain/include/domain/ImportModule.hpp | 20 ++++++- .../relinker/src/guest/GuestImageReader.cpp | 4 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 4 +- .../src/pipeline/RelinkerPipeline.cpp | 2 +- .../tests/test_guest_module_directories.py | 2 +- .../tests/test_windows_import_modules.py | 52 ++++++++++++++++--- 6 files changed, 70 insertions(+), 14 deletions(-) diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index fb78c079a..8da345c27 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -2,11 +2,21 @@ #define DOMAIN_IMPORTMODULE_HPP #include +#include +#include #include namespace Domain { -inline std::string ImportModule(const std::string& symbol, const std::map& modules) { +inline std::string ImportModuleName(std::string name) { + if (name.ends_with(".prx")) name.resize(name.size() - 4); + if (name.ends_with("-module")) name.resize(name.size() - 7); + std::replace(name.begin(), name.end(), '.', '_'); + return name; +} + +inline std::string ImportModule(const std::string& symbol, const std::map& modules, + std::span dependencies = {}) { if (modules.empty()) return {}; const auto first = symbol.find('#'); if (first == std::string::npos) return {}; @@ -27,6 +37,14 @@ inline std::string ImportModule(const std::string& symbol, const std::map importModules; for (const auto value : moduleImports) if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); + std::vector neededLibraries; + for (const auto offset : needed) neededLibraries.push_back(string(offset)); std::set exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); - const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules) : std::string{}; + const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules, neededLibraries) : std::string{}; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 9010692d3..e433f47b0 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -117,8 +117,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } std::vector> dependencies(images.size()); for (auto& image : images) { - image.UsePlatformTlsResolver = std::none_of(image.Symbols.begin(), image.Symbols.end(), [](const auto& symbol) { - return symbol.Name == "vNe1w4diLCs" && symbol.Section != 0 && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2; + image.UsePlatformTlsResolver = !windows ? !exports.contains("vNe1w4diLCs") : std::none_of(image.Symbols.begin(), image.Symbols.end(), [](const auto& symbol) { + return symbol.Name == "vNe1w4diLCs" && symbol.Section != 0 && symbol.Section != AbsoluteSection && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2; }); } for (std::size_t index = 0; index < images.size(); ++index) { diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 90f154767..1cb1f6c22 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -211,7 +211,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&nameOff, raw.data() + symOff, 4); const auto name = readCStr(nameOff); - nidRefs.push_back({name, Domain::ImportModule(name, importModules), relType, pos, rOffset, rAddend}); + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); } }; diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index e1a1c0aae..e01b5dce0 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -131,7 +131,7 @@ def main(): (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) (case / "prx" / "consumer.prx").write_bytes(module_with_symbol(False)) result, output = convert(case, windows) - assert result.returncode == 2 and "Ambiguous guest import shared" in result.stderr, result.stderr + assert result.returncode == 2 and "Ambiguous guest import" in result.stderr and "shared" in result.stderr, result.stderr assert not output.exists() and not (case / "app0").exists(), output print("Guest module directory integration tests passed") diff --git a/core/relinker/relinker/tests/test_windows_import_modules.py b/core/relinker/relinker/tests/test_windows_import_modules.py index a83858e5c..8b25b1ee9 100644 --- a/core/relinker/relinker/tests/test_windows_import_modules.py +++ b/core/relinker/relinker/tests/test_windows_import_modules.py @@ -29,29 +29,42 @@ def provider(value, soname=None): return image -def consumer(owner, symbol='shared#A#B', expected=22): +def consumer(owner, symbol='shared#A#B', expected=22, module_name=None): image = module_with_symbol(False) strings = b'\0' + symbol.encode() + b'\0' + owner.encode() + b'\0' owner_offset = len(symbol) + 2 + module_offset = owner_offset + if module_name is not None: + module_offset = len(strings) + strings += module_name.encode() + b'\0' image[0x800:0x800 + len(strings)] = strings tags = [(5, 0x2200), (10, len(strings)), (6, 0x2280), (11, 24), (4, 0x2240), (7, 0x2300), (8, 24), (9, 24), (12, 0x1010), - (1, owner_offset), (0x61000045, (1 << 48) | owner_offset)] + (1, owner_offset), (0x61000045, (1 << 48) | module_offset)] dynamic(image, 0x600, 176, tags) code = b'\xff\x15' + struct.pack(' Date: Mon, 5 Oct 2026 16:50:22 +0200 Subject: [PATCH 026/203] refactor(libs): drop replacements of modules that titles ship themselves Co-Authored-By: Claude --- CONTRIBUTING.md | 1 + core/libs/CMakeLists.txt | 21 - core/libs/prx/libREDGalaxy64/Export.cpp | 196 -- core/libs/prx/libRenoirCore.PS5/Export.cpp | 145 -- .../libs/prx/libSceNpCppWebApi/CMakeLists.txt | 21 - core/libs/prx/libSceNpCppWebApi/Export.cpp | 1194 ----------- core/libs/prx/libcohtml/CMakeLists.txt | 6 - core/libs/prx/libcohtml/DEPLOY.md | 16 - core/libs/prx/libcohtml/Export.cpp | 307 --- core/libs/prx/libfmod/CMakeLists.txt | 11 - core/libs/prx/libfmod/Export.cpp | 1902 ----------------- core/libs/prx/libfmodstudio/CMakeLists.txt | 10 - core/libs/prx/libfmodstudio/Export.cpp | 1007 --------- .../tests/GuestNpCppWebApiIntrusivePtr.cpp | 256 --- core/libs/tests/GuestNpCppWebApiString.cpp | 112 - docs/dev/TechnicalDebt.md | 2 - tools/package_release.py | 1 - 17 files changed, 1 insertion(+), 5207 deletions(-) delete mode 100644 core/libs/prx/libREDGalaxy64/Export.cpp delete mode 100644 core/libs/prx/libRenoirCore.PS5/Export.cpp delete mode 100644 core/libs/prx/libSceNpCppWebApi/CMakeLists.txt delete mode 100644 core/libs/prx/libSceNpCppWebApi/Export.cpp delete mode 100644 core/libs/prx/libcohtml/CMakeLists.txt delete mode 100644 core/libs/prx/libcohtml/DEPLOY.md delete mode 100644 core/libs/prx/libcohtml/Export.cpp delete mode 100644 core/libs/prx/libfmod/CMakeLists.txt delete mode 100644 core/libs/prx/libfmod/Export.cpp delete mode 100644 core/libs/prx/libfmodstudio/CMakeLists.txt delete mode 100644 core/libs/prx/libfmodstudio/Export.cpp delete mode 100644 core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp delete mode 100644 core/libs/tests/GuestNpCppWebApiString.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a34bd71a5..c12a36055 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ - A silent stub is allowed only when it unblocks a title and only affects the UI; add it to [silent stubs](docs/dev/TechnicalDebt.md#silent-stubs). - Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running. - Implement the general behaviour of a function, not what one title happens to need. +- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries (NpCppWebApi over NpWebApi2). The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). - Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix. - The relinker uses only the C++20 standard library. - Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 510110335..dfe86ccd6 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -118,17 +118,6 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() -set(LIBCOHTML_PATCHED ${PATCHED_DIR}/libcohtml.prx) -set(LIBCOHTML_PROSPERO_PATCHED ${PATCHED_DIR}/libcohtml.Prospero.prx) -add_custom_command( - OUTPUT ${LIBCOHTML_PROSPERO_PATCHED} - COMMAND ${CMAKE_COMMAND} -E copy "${LIBCOHTML_PATCHED}" "${LIBCOHTML_PROSPERO_PATCHED}" - DEPENDS ${LIBCOHTML_PATCHED} - VERBATIM -) -add_custom_target(libcohtml_prospero DEPENDS ${LIBCOHTML_PROSPERO_PATCHED}) -list(APPEND PATCHED_OUTPUTS ${LIBCOHTML_PROSPERO_PATCHED}) - add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) @@ -170,16 +159,6 @@ if(BUILD_TESTING) add_test(NAME host_thread_local COMMAND host_thread_local_tests) set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15) - add_test_executable(guest_np_cpp_web_api_intrusive_ptr_tests tests/GuestNpCppWebApiIntrusivePtr.cpp) - target_include_directories(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE libSceNpCppWebApi libc) - configure_windows_unwind(guest_np_cpp_web_api_intrusive_ptr_tests) - add_test(NAME guest_np_cpp_web_api_intrusive_ptr COMMAND guest_np_cpp_web_api_intrusive_ptr_tests) - if(WIN32) - set_tests_properties(guest_np_cpp_web_api_intrusive_ptr PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() - add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData) diff --git a/core/libs/prx/libREDGalaxy64/Export.cpp b/core/libs/prx/libREDGalaxy64/Export.cpp deleted file mode 100644 index bf5c6235f..000000000 --- a/core/libs/prx/libREDGalaxy64/Export.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -// CD Projekt RED GOG Galaxy wrapper, bundled with the game as -// modules/libREDGalaxy64.prx (seen in Cyberpunk 2077 PPSA04029). -// Online Galaxy services cannot work under emulation: Init/Shutdown/ -// ProcessData are accepted and ignored, interface getters return null -// (all observed engine call sites null-check the result). -// Roles below were reversed from the module's own export bodies: -// Init references clientID/clientSecret/configFilePath, Shutdown -// releases the facade singletons, ProcessData throttles facade -// ProcessData calls, GetErrorString formats Gog/Std exceptions. - -APS5_EXPORT("1icN9EO2WSg", redGalaxy64Init); -std::uint64_t APS5_VABI redGalaxy64Init(const void* options) { - (void)options; - NotImplemented_nid_no_patch(__func__); - return 1; -} - -APS5_EXPORT("SA7aCTRE5zg", redGalaxy64Shutdown); -void APS5_VABI redGalaxy64Shutdown(void) { - NotImplemented_nid_no_patch(__func__); -} - -APS5_EXPORT("ZVPOpEqpUvo", redGalaxy64ProcessData); -void APS5_VABI redGalaxy64ProcessData(void) { - NotImplemented_nid_no_patch(__func__); -} - -APS5_EXPORT("4trn+7BssaY", redGalaxy64GetErrorString); -const char* APS5_VABI redGalaxy64GetErrorString(void) { - NotImplemented_nid_no_patch(__func__); - return ""; -} - -APS5_EXPORT("DQAUzFVaGl8", redGalaxy64GameServerInit); -std::uint64_t APS5_VABI redGalaxy64GameServerInit(const void* options) { - (void)options; - NotImplemented_nid_no_patch(__func__); - return 1; -} - -APS5_EXPORT("GKomji4WVHw", redGalaxy64ProcessGameServerData); -void APS5_VABI redGalaxy64ProcessGameServerData(void) { - NotImplemented_nid_no_patch(__func__); -} - -APS5_EXPORT("36lort-kFp4", redgalaxy_stub_00); -std::uint64_t APS5_VABI redgalaxy_stub_00(std::uint64_t a0, std::uint64_t a1, std::uint64_t a2) { - (void)a0; - (void)a1; - (void)a2; - NotImplemented_nid_no_patch("36lort-kFp4"); - return 0; -} - -APS5_EXPORT("NgDLhCc6F7Y", redgalaxy_stub_01); -std::uint64_t APS5_VABI redgalaxy_stub_01(const void* a0, const char* a1, std::uint64_t a2) { - (void)a0; - (void)a1; - (void)a2; - NotImplemented_nid_no_patch("NgDLhCc6F7Y"); - return 0; -} - -APS5_EXPORT("huJ+beHcWBE", redgalaxy_stub_02); -std::uint64_t APS5_VABI redgalaxy_stub_02(void) { - NotImplemented_nid_no_patch("huJ+beHcWBE"); - return 0; -} - -APS5_EXPORT("sxiZFliOEDU", redgalaxy_stub_03); -std::uint64_t APS5_VABI redgalaxy_stub_03(void) { - NotImplemented_nid_no_patch("sxiZFliOEDU"); - return 0; -} - -APS5_EXPORT("49Bdn1OOJ90", redgalaxy_stub_04); -std::uint64_t APS5_VABI redgalaxy_stub_04(void) { - NotImplemented_nid_no_patch("49Bdn1OOJ90"); - return 0; -} - -APS5_EXPORT("8ahh9DUqZLY", redgalaxy_stub_05); -std::uint64_t APS5_VABI redgalaxy_stub_05(void) { - NotImplemented_nid_no_patch("8ahh9DUqZLY"); - return 0; -} - -APS5_EXPORT("Ard4nEfw5Vs", redgalaxy_stub_06); -std::uint64_t APS5_VABI redgalaxy_stub_06(void) { - NotImplemented_nid_no_patch("Ard4nEfw5Vs"); - return 0; -} - -APS5_EXPORT("AzO2fwK+MUs", redgalaxy_stub_07); -std::uint64_t APS5_VABI redgalaxy_stub_07(void) { - NotImplemented_nid_no_patch("AzO2fwK+MUs"); - return 0; -} - -APS5_EXPORT("HKIwQPq45o4", redgalaxy_stub_08); -std::uint64_t APS5_VABI redgalaxy_stub_08(void) { - NotImplemented_nid_no_patch("HKIwQPq45o4"); - return 0; -} - -APS5_EXPORT("IXRuom5imkM", redgalaxy_stub_09); -std::uint64_t APS5_VABI redgalaxy_stub_09(void) { - NotImplemented_nid_no_patch("IXRuom5imkM"); - return 0; -} - -APS5_EXPORT("LjDqfncevos", redgalaxy_stub_10); -std::uint64_t APS5_VABI redgalaxy_stub_10(void) { - NotImplemented_nid_no_patch("LjDqfncevos"); - return 0; -} - -APS5_EXPORT("Onwg0aGsFhM", redgalaxy_stub_11); -std::uint64_t APS5_VABI redgalaxy_stub_11(void) { - NotImplemented_nid_no_patch("Onwg0aGsFhM"); - return 0; -} - -APS5_EXPORT("oSyqfoYcK40", redgalaxy_stub_12); -std::uint64_t APS5_VABI redgalaxy_stub_12(void) { - NotImplemented_nid_no_patch("oSyqfoYcK40"); - return 0; -} - -APS5_EXPORT("qoS0gqW7RxI", redgalaxy_stub_13); -std::uint64_t APS5_VABI redgalaxy_stub_13(void) { - NotImplemented_nid_no_patch("qoS0gqW7RxI"); - return 0; -} - -APS5_EXPORT("SOtPNNwajSw", redgalaxy_stub_14); -std::uint64_t APS5_VABI redgalaxy_stub_14(void) { - NotImplemented_nid_no_patch("SOtPNNwajSw"); - return 0; -} - -APS5_EXPORT("udfpd9vbE+w", redgalaxy_stub_15); -std::uint64_t APS5_VABI redgalaxy_stub_15(void) { - NotImplemented_nid_no_patch("udfpd9vbE+w"); - return 0; -} - -APS5_EXPORT("vuGujcpcUEk", redgalaxy_stub_16); -std::uint64_t APS5_VABI redgalaxy_stub_16(void) { - NotImplemented_nid_no_patch("vuGujcpcUEk"); - return 0; -} - -APS5_EXPORT("VXUTdiaHbD4", redgalaxy_stub_17); -std::uint64_t APS5_VABI redgalaxy_stub_17(void) { - NotImplemented_nid_no_patch("VXUTdiaHbD4"); - return 0; -} - -APS5_EXPORT("w5-trCio4bQ", redgalaxy_stub_18); -std::uint64_t APS5_VABI redgalaxy_stub_18(void) { - NotImplemented_nid_no_patch("w5-trCio4bQ"); - return 0; -} - -APS5_EXPORT("YpZjREtZb20", redgalaxy_stub_19); -std::uint64_t APS5_VABI redgalaxy_stub_19(void) { - NotImplemented_nid_no_patch("YpZjREtZb20"); - return 0; -} - -APS5_EXPORT("gPJN5c9XCec", redgalaxy_stub_20); -std::uint64_t APS5_VABI redgalaxy_stub_20(void) { - NotImplemented_nid_no_patch("gPJN5c9XCec"); - return 0; -} - -APS5_EXPORT("z4SpWc89no8", redgalaxy_stub_21); -std::uint64_t APS5_VABI redgalaxy_stub_21(void) { - NotImplemented_nid_no_patch("z4SpWc89no8"); - return 0; -} - -APS5_EXPORT("zdzopKdBeU4", redgalaxy_stub_22); -std::uint64_t APS5_VABI redgalaxy_stub_22(void) { - NotImplemented_nid_no_patch("zdzopKdBeU4"); - return 0; -} - -} diff --git a/core/libs/prx/libRenoirCore.PS5/Export.cpp b/core/libs/prx/libRenoirCore.PS5/Export.cpp deleted file mode 100644 index c0cf955a3..000000000 --- a/core/libs/prx/libRenoirCore.PS5/Export.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -APS5_EXPORT("0DsDTr534gc", renoirStub00); -uint64_t APS5_VABI renoirStub00(void) { - NotImplemented_nid_no_patch("0DsDTr534gc"); - return 0; -} - -APS5_EXPORT("2T56HgbPW08", renoirStub01); -uint64_t APS5_VABI renoirStub01(void) { - NotImplemented_nid_no_patch("2T56HgbPW08"); - return 0; -} - -APS5_EXPORT("3HnCozh8Az8", renoirStub02); -uint64_t APS5_VABI renoirStub02(void) { - NotImplemented_nid_no_patch("3HnCozh8Az8"); - return 0; -} - -APS5_EXPORT("5Ysce+s-CWU", renoirStub03); -uint64_t APS5_VABI renoirStub03(void) { - NotImplemented_nid_no_patch("5Ysce+s-CWU"); - return 0; -} - -APS5_EXPORT("AdlSqA4lGS8", renoirStub04); -uint64_t APS5_VABI renoirStub04(void) { - NotImplemented_nid_no_patch("AdlSqA4lGS8"); - return 0; -} - -APS5_EXPORT("bC37hk7rHqs", renoirStub05); -uint64_t APS5_VABI renoirStub05(void) { - NotImplemented_nid_no_patch("bC37hk7rHqs"); - return 0; -} - -APS5_EXPORT("dfPK9R6M0qM", renoirStub06); -uint64_t APS5_VABI renoirStub06(void) { - NotImplemented_nid_no_patch("dfPK9R6M0qM"); - return 0; -} - -APS5_EXPORT("fZiBbVa2ZDU", renoirStub07); -uint64_t APS5_VABI renoirStub07(void) { - NotImplemented_nid_no_patch("fZiBbVa2ZDU"); - return 0; -} - -APS5_EXPORT("hkWBH8-WfW4", renoirStub08); -uint64_t APS5_VABI renoirStub08(void) { - NotImplemented_nid_no_patch("hkWBH8-WfW4"); - return 0; -} - -APS5_EXPORT("hUxvGdi64Io", renoirStub09); -uint64_t APS5_VABI renoirStub09(void) { - NotImplemented_nid_no_patch("hUxvGdi64Io"); - return 0; -} - -APS5_EXPORT("NxqsZxGisrA", renoirStub10); -uint64_t APS5_VABI renoirStub10(void) { - NotImplemented_nid_no_patch("NxqsZxGisrA"); - return 0; -} - -APS5_EXPORT("OgZVIkaNmb0", renoirStub11); -uint64_t APS5_VABI renoirStub11(void) { - NotImplemented_nid_no_patch("OgZVIkaNmb0"); - return 0; -} - -APS5_EXPORT("ojLQdRUnVgA", renoirStub12); -uint64_t APS5_VABI renoirStub12(void) { - NotImplemented_nid_no_patch("ojLQdRUnVgA"); - return 0; -} - -APS5_EXPORT("s0qDfgsAgVg", renoirStub13); -uint64_t APS5_VABI renoirStub13(void) { - NotImplemented_nid_no_patch("s0qDfgsAgVg"); - return 0; -} - -APS5_EXPORT("sgrd-r1Nw60", renoirStub14); -uint64_t APS5_VABI renoirStub14(void) { - NotImplemented_nid_no_patch("sgrd-r1Nw60"); - return 0; -} - -APS5_EXPORT("sjTRb8YD7dQ", renoirStub15); -uint64_t APS5_VABI renoirStub15(void) { - NotImplemented_nid_no_patch("sjTRb8YD7dQ"); - return 0; -} - -APS5_EXPORT("tG86XGc2VtE", renoirStub16); -uint64_t APS5_VABI renoirStub16(void) { - NotImplemented_nid_no_patch("tG86XGc2VtE"); - return 0; -} - -APS5_EXPORT("umlGxOcNzgc", renoirStub17); -uint64_t APS5_VABI renoirStub17(void) { - NotImplemented_nid_no_patch("umlGxOcNzgc"); - return 0; -} - -APS5_EXPORT("vPxA9OFW0os", renoirStub18); -uint64_t APS5_VABI renoirStub18(void) { - NotImplemented_nid_no_patch("vPxA9OFW0os"); - return 0; -} - -APS5_EXPORT("X2qEBlOLkNo", renoirStub19); -uint64_t APS5_VABI renoirStub19(void) { - NotImplemented_nid_no_patch("X2qEBlOLkNo"); - return 0; -} - -APS5_EXPORT("ynh4E+BIaqY", renoirStub20); -uint64_t APS5_VABI renoirStub20(void) { - NotImplemented_nid_no_patch("ynh4E+BIaqY"); - return 0; -} - -APS5_EXPORT("YXe6ABbHZPs", renoirStub21); -uint64_t APS5_VABI renoirStub21(void) { - NotImplemented_nid_no_patch("YXe6ABbHZPs"); - return 0; -} - -APS5_EXPORT("z0jsXZVz75I", renoirStub22); -uint64_t APS5_VABI renoirStub22(void) { - NotImplemented_nid_no_patch("z0jsXZVz75I"); - return 0; -} - -} diff --git a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt deleted file mode 100644 index 2519e8cc8..000000000 --- a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -add_library(libSceNpCppWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceNpCppWebApi PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceNpCppWebApi) - -if(MINGW) - foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) - string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") - endforeach() -endif() - -if(BUILD_TESTING) - add_test_executable(guest_np_cpp_web_api_string_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCppWebApiString.cpp) - target_include_directories(guest_np_cpp_web_api_string_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_cpp_web_api_string_tests PRIVATE libSceNpCppWebApi libc) - configure_windows_unwind(guest_np_cpp_web_api_string_tests) - add_test(NAME guest_np_cpp_web_api_string COMMAND guest_np_cpp_web_api_string_tests) - if(WIN32) - set_tests_properties(guest_np_cpp_web_api_string PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() -endif() diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp deleted file mode 100644 index 75caf2847..000000000 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ /dev/null @@ -1,1194 +0,0 @@ -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -static constexpr int SCE_NP_WEBAPI_ERROR_UNAVAILABLE = static_cast(0x80552901); - -// Offline PSN: request entry points (API calls, transaction start/readData, factories) fail with -// SCE_NP_WEBAPI_ERROR_UNAVAILABLE; parameter bookkeeping succeeds; response accessors stay unimplemented -// because a failed request never produces a response. -extern "C" { - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V111UserFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_4UserEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody18setStartSerialRankERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setGroupERKNS3_5GroupE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setLimitERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setUsersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_4UserEEEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody9setOffsetERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody10getEntriesEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody10setCommentEPKc() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody12setSmallDataEPKvm() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody15setNeedsTmpRankERKb() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody19setComparedDateTimeERK10SceRtcTick() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody7setPcIdERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V128GetRankingRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V129RecordScoreRequestBodyFactory6createEPNS1_6Common10LibContextElPNS5_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User12setAccountIdERKm() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User7setPcIdERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi10getRankingEiRKNS4_21ParameterToGetRankingERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_22GetRankingResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking10initializeEPNS1_6Common10LibContextEi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking24setgetRankingRequestBodyENS1_6Common12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi22getLargeDataByObjectIdEiRKNS4_33ParameterToGetLargeDataByObjectIdERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_37GetLargeDataByObjectIdResponseHeadersEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId10initializeEPNS1_6Common10LibContextEPKc() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi18getBoardDefinitionEiRKNS4_29ParameterToGetBoardDefinitionERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30GetBoardDefinitionResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition10initializeEPNS1_6Common10LibContextEi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi11recordScoreEiRKNS4_22ParameterToRecordScoreERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23RecordScoreResponseBodyEEENSA_INS4_26RecordScoreResponseHeadersEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi15recordLargeDataEiRKNS4_26ParameterToRecordLargeDataERNS1_6Common19UpStreamTransactionINS8_12IntrusivePtrINS3_27RecordLargeDataResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore10initializeEPNS1_6Common10LibContextEiNS6_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore22setxPsnAtomicOperationENS5_19XPsnAtomicOperationE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData10initializeEPNS1_6Common10LibContextEiNS5_19XPsnAtomicOperationEPKc() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi12downloadDataEiRKNS4_23ParameterToDownloadDataERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_27DownloadDataResponseHeadersEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10LibContextC1Ev(uint64_t* self) { - *self = 0; - return 0; -} - -// Initialization succeeds as on an offline console (see libSceJson2); individual requests fail. -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(const void* params, uint64_t* context) { - (void)params; - if (context) *context = 1; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { - (void)self; - return 0; -} - -struct IntrusivePtr { - void* object; - void (APS5_VABI* deleter)(void*); - void* libContext; -}; -static_assert(sizeof(IntrusivePtr) == 0x18); - -static constexpr std::size_t ReferenceCountOffset = 0; -static constexpr std::size_t VectorReferenceCountOffset = 0x30; - -static std::atomic_ref ReferenceCount(void* object, const std::size_t offset) { - return std::atomic_ref(*reinterpret_cast(static_cast(object) + offset)); -} - -static void CopyIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* source, const std::size_t referenceCountOffset) { - *self = *source; - if (self->object) ReferenceCount(self->object, referenceCountOffset).fetch_add(1); -} - -static void ReleaseIntrusivePtr(IntrusivePtr* self, const std::size_t referenceCountOffset, const char* function) { - if (!self->object) return; - if (ReferenceCount(self->object, referenceCountOffset).fetch_sub(1) != 1) return; - if (!self->deleter) NotImplemented_nid_no_patch(function); - self->deleter(self->object); - self->object = nullptr; - self->deleter = nullptr; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { - CopyIntrusivePtr(self, source, ReferenceCountOffset); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { - CopyIntrusivePtr(self, source, ReferenceCountOffset); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEED2Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextEm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE8sendDataEPKvm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE8readDataEPcm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE8readDataEPcm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEED1Ev(void* self) { - (void)self; - return 0; -} - -struct CommonString { - std::int32_t referenceCount; - char* buffer; - std::uint64_t bufferSize; - void* libContext; -}; -static_assert(sizeof(CommonString) == 0x20 && offsetof(CommonString, buffer) == 0x08); -static_assert(offsetof(CommonString, bufferSize) == 0x10 && offsetof(CommonString, libContext) == 0x18); - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(CommonString* self, void* libContext) { - self->referenceCount = 0; - self->buffer = nullptr; - self->bufferSize = 0; - self->libContext = libContext; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(CommonString* self) { - if (self->buffer != nullptr) NotImplemented_nid_no_patch("sce::Np::CppWebApi::Common::String buffer release"); -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEE8pushBackERKS8_() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEEC1EPNS2_10LibContextE(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE3endEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE5beginEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEppEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common9terminateERNS2_10LibContextE(uint64_t* context) { - *context = 0; - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody22getLastUpdatedDateTimeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody10getTmpRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody16getTmpSerialRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody11getSortModeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getEntryLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getUpdateModeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13sortModeIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15entryLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15updateModeIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMaxScoreLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMinScoreLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18maxScoreLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18minScoreLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody20getLargeDataNumLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody21getLargeDataSizeLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody22largeDataNumLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody23largeDataSizeLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry10getCommentEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getObjectIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getOnlineIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12commentIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getAccountIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getSmallDataEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13getSerialRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13objectIdIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14getHighestRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14smallDataIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry20getHighestSerialRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getPcIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry8getScoreEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V19RecordApi26RecordScoreResponseHeaders24getXPsnAtomicOperationIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE11getResponseERS8_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr* self) { - return self->object; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE18getResponseHeadersERSB_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary4sizeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary9getBinaryEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const CommonString* self) { - return self->buffer != nullptr ? self->buffer : ""; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEdeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEneERKS9_() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - - -APS5_EXPORT("+gSMGXHgrPU", sceNpCppWebApiUnknown00); -int APS5_VABI sceNpCppWebApiUnknown00(void) { - NotImplemented_nid_no_patch("+gSMGXHgrPU"); - return 0; -} - -APS5_EXPORT("-XNfwZTFkWw", sceNpCppWebApiUnknown01); -int APS5_VABI sceNpCppWebApiUnknown01(void) { - NotImplemented_nid_no_patch("-XNfwZTFkWw"); - return 0; -} - -APS5_EXPORT("0f5+eBcgB14", sceNpCppWebApiUnknown02); -int APS5_VABI sceNpCppWebApiUnknown02(void) { - NotImplemented_nid_no_patch("0f5+eBcgB14"); - return 0; -} - -APS5_EXPORT("1iq5Jtw4jVs", sceNpCppWebApiUnknown03); -int APS5_VABI sceNpCppWebApiUnknown03(void) { - NotImplemented_nid_no_patch("1iq5Jtw4jVs"); - return 0; -} - -APS5_EXPORT("1wpCXuFzH10", sceNpCppWebApiUnknown04); -int APS5_VABI sceNpCppWebApiUnknown04(void) { - NotImplemented_nid_no_patch("1wpCXuFzH10"); - return 0; -} - -APS5_EXPORT("4AFThLW28xQ", sceNpCppWebApiUnknown05); -int APS5_VABI sceNpCppWebApiUnknown05(void) { - NotImplemented_nid_no_patch("4AFThLW28xQ"); - return 0; -} - -APS5_EXPORT("8-9Y4oS+OuE", sceNpCppWebApiUnknown06); -int APS5_VABI sceNpCppWebApiUnknown06(void) { - NotImplemented_nid_no_patch("8-9Y4oS+OuE"); - return 0; -} - -APS5_EXPORT("8dWkWjo5EeE", sceNpCppWebApiUnknown07); -int APS5_VABI sceNpCppWebApiUnknown07(void) { - NotImplemented_nid_no_patch("8dWkWjo5EeE"); - return 0; -} - -APS5_EXPORT("BHvfIGdarBQ", sceNpCppWebApiUnknown08); -int APS5_VABI sceNpCppWebApiUnknown08(void) { - NotImplemented_nid_no_patch("BHvfIGdarBQ"); - return 0; -} - -APS5_EXPORT("Fxxr5lYBfl4", sceNpCppWebApiUnknown09); -int APS5_VABI sceNpCppWebApiUnknown09(void) { - NotImplemented_nid_no_patch("Fxxr5lYBfl4"); - return 0; -} - -APS5_EXPORT("M1LnjRscx-w", sceNpCppWebApiUnknown10); -int APS5_VABI sceNpCppWebApiUnknown10(void) { - NotImplemented_nid_no_patch("M1LnjRscx-w"); - return 0; -} - -APS5_EXPORT("PsX657reZeo", sceNpCppWebApiUnknown11); -int APS5_VABI sceNpCppWebApiUnknown11(void) { - NotImplemented_nid_no_patch("PsX657reZeo"); - return 0; -} - -APS5_EXPORT("RvH9AZ5rCkQ", sceNpCppWebApiUnknown12); -int APS5_VABI sceNpCppWebApiUnknown12(void) { - NotImplemented_nid_no_patch("RvH9AZ5rCkQ"); - return 0; -} - -APS5_EXPORT("SR3BKonD8yk", sceNpCppWebApiUnknown13); -int APS5_VABI sceNpCppWebApiUnknown13(void) { - NotImplemented_nid_no_patch("SR3BKonD8yk"); - return 0; -} - -APS5_EXPORT("TSoQhYKyq5g", sceNpCppWebApiUnknown14); -int APS5_VABI sceNpCppWebApiUnknown14(void) { - NotImplemented_nid_no_patch("TSoQhYKyq5g"); - return 0; -} - -APS5_EXPORT("TgsJjQqp+2k", sceNpCppWebApiUnknown15); -int APS5_VABI sceNpCppWebApiUnknown15(void) { - NotImplemented_nid_no_patch("TgsJjQqp+2k"); - return 0; -} - -APS5_EXPORT("UHQxE3HhTXA", sceNpCppWebApiUnknown16); -int APS5_VABI sceNpCppWebApiUnknown16(void) { - NotImplemented_nid_no_patch("UHQxE3HhTXA"); - return 0; -} - -APS5_EXPORT("ZN-glpk0Rug", sceNpCppWebApiUnknown17); -int APS5_VABI sceNpCppWebApiUnknown17(void) { - NotImplemented_nid_no_patch("ZN-glpk0Rug"); - return 0; -} - -APS5_EXPORT("ZQzXwokCnUE", sceNpCppWebApiUnknown18); -int APS5_VABI sceNpCppWebApiUnknown18(void) { - NotImplemented_nid_no_patch("ZQzXwokCnUE"); - return 0; -} - -APS5_EXPORT("ZwjmbSr4Cxc", sceNpCppWebApiUnknown19); -int APS5_VABI sceNpCppWebApiUnknown19(void) { - NotImplemented_nid_no_patch("ZwjmbSr4Cxc"); - return 0; -} - -APS5_EXPORT("b7leY-LNVnI", sceNpCppWebApiUnknown20); -int APS5_VABI sceNpCppWebApiUnknown20(void) { - NotImplemented_nid_no_patch("b7leY-LNVnI"); - return 0; -} - -APS5_EXPORT("c3J9K9XbQqE", sceNpCppWebApiUnknown21); -int APS5_VABI sceNpCppWebApiUnknown21(void) { - NotImplemented_nid_no_patch("c3J9K9XbQqE"); - return 0; -} - -APS5_EXPORT("cSLbQUQO2Ns", sceNpCppWebApiUnknown22); -int APS5_VABI sceNpCppWebApiUnknown22(void) { - NotImplemented_nid_no_patch("cSLbQUQO2Ns"); - return 0; -} - -APS5_EXPORT("cqOxjL0ZfFA", sceNpCppWebApiUnknown23); -int APS5_VABI sceNpCppWebApiUnknown23(void) { - NotImplemented_nid_no_patch("cqOxjL0ZfFA"); - return 0; -} - -APS5_EXPORT("d1eEjWR60wk", sceNpCppWebApiUnknown24); -int APS5_VABI sceNpCppWebApiUnknown24(void) { - NotImplemented_nid_no_patch("d1eEjWR60wk"); - return 0; -} - -APS5_EXPORT("epwr+cBCIFs", sceNpCppWebApiUnknown25); -int APS5_VABI sceNpCppWebApiUnknown25(void) { - NotImplemented_nid_no_patch("epwr+cBCIFs"); - return 0; -} - -APS5_EXPORT("eruduJ0KrT0", sceNpCppWebApiUnknown26); -int APS5_VABI sceNpCppWebApiUnknown26(void) { - NotImplemented_nid_no_patch("eruduJ0KrT0"); - return 0; -} - -APS5_EXPORT("fG06a38iao8", sceNpCppWebApiUnknown27); -int APS5_VABI sceNpCppWebApiUnknown27(void) { - NotImplemented_nid_no_patch("fG06a38iao8"); - return 0; -} - -APS5_EXPORT("jfpSx14AeLc", sceNpCppWebApiUnknown28); -int APS5_VABI sceNpCppWebApiUnknown28(void) { - NotImplemented_nid_no_patch("jfpSx14AeLc"); - return 0; -} - -APS5_EXPORT("kBxwE4YfbIM", sceNpCppWebApiUnknown29); -int APS5_VABI sceNpCppWebApiUnknown29(void) { - NotImplemented_nid_no_patch("kBxwE4YfbIM"); - return 0; -} - -APS5_EXPORT("kz2Z38yTLq0", sceNpCppWebApiUnknown30); -int APS5_VABI sceNpCppWebApiUnknown30(void) { - NotImplemented_nid_no_patch("kz2Z38yTLq0"); - return 0; -} - -APS5_EXPORT("lB8driFKaoU", sceNpCppWebApiUnknown31); -int APS5_VABI sceNpCppWebApiUnknown31(void) { - NotImplemented_nid_no_patch("lB8driFKaoU"); - return 0; -} - -APS5_EXPORT("mCsV7izOkjY", sceNpCppWebApiUnknown32); -int APS5_VABI sceNpCppWebApiUnknown32(void) { - NotImplemented_nid_no_patch("mCsV7izOkjY"); - return 0; -} - -APS5_EXPORT("n1yCLMxpseQ", sceNpCppWebApiUnknown33); -int APS5_VABI sceNpCppWebApiUnknown33(void) { - NotImplemented_nid_no_patch("n1yCLMxpseQ"); - return 0; -} - -APS5_EXPORT("nL9vSVq-29s", sceNpCppWebApiUnknown34); -int APS5_VABI sceNpCppWebApiUnknown34(void) { - NotImplemented_nid_no_patch("nL9vSVq-29s"); - return 0; -} - -APS5_EXPORT("nmz5JYKcMfY", sceNpCppWebApiUnknown35); -int APS5_VABI sceNpCppWebApiUnknown35(void) { - NotImplemented_nid_no_patch("nmz5JYKcMfY"); - return 0; -} - -APS5_EXPORT("oHRl7a+zdMU", sceNpCppWebApiUnknown36); -int APS5_VABI sceNpCppWebApiUnknown36(void) { - NotImplemented_nid_no_patch("oHRl7a+zdMU"); - return 0; -} - -APS5_EXPORT("pSpO3hPNv64", sceNpCppWebApiUnknown37); -int APS5_VABI sceNpCppWebApiUnknown37(void) { - NotImplemented_nid_no_patch("pSpO3hPNv64"); - return 0; -} - -APS5_EXPORT("q4UFICay6Hk", sceNpCppWebApiUnknown38); -int APS5_VABI sceNpCppWebApiUnknown38(void) { - NotImplemented_nid_no_patch("q4UFICay6Hk"); - return 0; -} - -APS5_EXPORT("qiD2PU2Jstc", sceNpCppWebApiUnknown39); -int APS5_VABI sceNpCppWebApiUnknown39(void) { - NotImplemented_nid_no_patch("qiD2PU2Jstc"); - return 0; -} - -APS5_EXPORT("tFe964qzGEM", sceNpCppWebApiUnknown40); -int APS5_VABI sceNpCppWebApiUnknown40(void) { - NotImplemented_nid_no_patch("tFe964qzGEM"); - return 0; -} - -APS5_EXPORT("tHaO36kincQ", sceNpCppWebApiUnknown41); -int APS5_VABI sceNpCppWebApiUnknown41(void) { - NotImplemented_nid_no_patch("tHaO36kincQ"); - return 0; -} - -APS5_EXPORT("umm5m+mXiZs", sceNpCppWebApiUnknown42); -int APS5_VABI sceNpCppWebApiUnknown42(void) { - NotImplemented_nid_no_patch("umm5m+mXiZs"); - return 0; -} - -APS5_EXPORT("uwhztB49KOQ", sceNpCppWebApiUnknown43); -int APS5_VABI sceNpCppWebApiUnknown43(void) { - NotImplemented_nid_no_patch("uwhztB49KOQ"); - return 0; -} - -APS5_EXPORT("w4K4nTYwhVE", sceNpCppWebApiUnknown44); -int APS5_VABI sceNpCppWebApiUnknown44(void) { - NotImplemented_nid_no_patch("w4K4nTYwhVE"); - return 0; -} - -APS5_EXPORT("wFcm6bgWB9Q", sceNpCppWebApiUnknown45); -int APS5_VABI sceNpCppWebApiUnknown45(void) { - NotImplemented_nid_no_patch("wFcm6bgWB9Q"); - return 0; -} - -APS5_EXPORT("wp8+c84G5Xw", sceNpCppWebApiUnknown46); -int APS5_VABI sceNpCppWebApiUnknown46(void) { - NotImplemented_nid_no_patch("wp8+c84G5Xw"); - return 0; -} - -APS5_EXPORT("zjqTmP0ST9A", sceNpCppWebApiUnknown47); -int APS5_VABI sceNpCppWebApiUnknown47(void) { - NotImplemented_nid_no_patch("zjqTmP0ST9A"); - return 0; -} - -APS5_EXPORT("zzO8ZGJ74ng", sceNpCppWebApiUnknown48); -int APS5_VABI sceNpCppWebApiUnknown48(void) { - NotImplemented_nid_no_patch("zzO8ZGJ74ng"); - return 0; -} - -APS5_EXPORT("12wY179+CE8", sceNpCppWebApiUnknown49); -int APS5_VABI sceNpCppWebApiUnknown49(void) { - NotImplemented_nid_no_patch("12wY179+CE8"); - return 0; -} - -APS5_EXPORT("3bjEFf8OhTs", sceNpCppWebApiUnknown50); -int APS5_VABI sceNpCppWebApiUnknown50(void) { - NotImplemented_nid_no_patch("3bjEFf8OhTs"); - return 0; -} - -APS5_EXPORT("6FK4IOnANkc", sceNpCppWebApiUnknown51); -int APS5_VABI sceNpCppWebApiUnknown51(void) { - NotImplemented_nid_no_patch("6FK4IOnANkc"); - return 0; -} - -APS5_EXPORT("EJAOHYWUki4", sceNpCppWebApiUnknown52); -int APS5_VABI sceNpCppWebApiUnknown52(void) { - NotImplemented_nid_no_patch("EJAOHYWUki4"); - return 0; -} - -APS5_EXPORT("FlagLhjAEmc", sceNpCppWebApiUnknown53); -int APS5_VABI sceNpCppWebApiUnknown53(void) { - NotImplemented_nid_no_patch("FlagLhjAEmc"); - return 0; -} - -APS5_EXPORT("G3h-NDHnyW4", sceNpCppWebApiUnknown54); -int APS5_VABI sceNpCppWebApiUnknown54(void) { - NotImplemented_nid_no_patch("G3h-NDHnyW4"); - return 0; -} - -APS5_EXPORT("H0XjNhflED4", sceNpCppWebApiUnknown55); -int APS5_VABI sceNpCppWebApiUnknown55(void) { - NotImplemented_nid_no_patch("H0XjNhflED4"); - return 0; -} - -APS5_EXPORT("IbWx007Acn4", sceNpCppWebApiUnknown56); -int APS5_VABI sceNpCppWebApiUnknown56(void) { - NotImplemented_nid_no_patch("IbWx007Acn4"); - return 0; -} - -APS5_EXPORT("Is+nI7Hq9jU", sceNpCppWebApiUnknown57); -int APS5_VABI sceNpCppWebApiUnknown57(void) { - NotImplemented_nid_no_patch("Is+nI7Hq9jU"); - return 0; -} - -APS5_EXPORT("Kc4x2uy0FFk", sceNpCppWebApiUnknown58); -int APS5_VABI sceNpCppWebApiUnknown58(void) { - NotImplemented_nid_no_patch("Kc4x2uy0FFk"); - return 0; -} - -APS5_EXPORT("OK+Ggpve7B0", sceNpCppWebApiUnknown59); -int APS5_VABI sceNpCppWebApiUnknown59(void) { - NotImplemented_nid_no_patch("OK+Ggpve7B0"); - return 0; -} - -APS5_EXPORT("P3daBLFFREw", sceNpCppWebApiUnknown60); -int APS5_VABI sceNpCppWebApiUnknown60(void) { - NotImplemented_nid_no_patch("P3daBLFFREw"); - return 0; -} - -APS5_EXPORT("RZcseJ3THfY", sceNpCppWebApiUnknown61); -int APS5_VABI sceNpCppWebApiUnknown61(void) { - NotImplemented_nid_no_patch("RZcseJ3THfY"); - return 0; -} - -APS5_EXPORT("RbvteD35X-8", sceNpCppWebApiUnknown62); -int APS5_VABI sceNpCppWebApiUnknown62(void) { - NotImplemented_nid_no_patch("RbvteD35X-8"); - return 0; -} - -APS5_EXPORT("XCtf+23QT0k", sceNpCppWebApiUnknown63); -int APS5_VABI sceNpCppWebApiUnknown63(void) { - NotImplemented_nid_no_patch("XCtf+23QT0k"); - return 0; -} - -APS5_EXPORT("a4q15LI1a4E", sceNpCppWebApiUnknown64); -int APS5_VABI sceNpCppWebApiUnknown64(void) { - NotImplemented_nid_no_patch("a4q15LI1a4E"); - return 0; -} - -APS5_EXPORT("eT4TQB7OsLA", sceNpCppWebApiUnknown65); -int APS5_VABI sceNpCppWebApiUnknown65(void) { - NotImplemented_nid_no_patch("eT4TQB7OsLA"); - return 0; -} - -APS5_EXPORT("g2dEqFnhFuw", sceNpCppWebApiUnknown66); -int APS5_VABI sceNpCppWebApiUnknown66(void) { - NotImplemented_nid_no_patch("g2dEqFnhFuw"); - return 0; -} - -APS5_EXPORT("lPAMjFZEpt8", sceNpCppWebApiUnknown67); -int APS5_VABI sceNpCppWebApiUnknown67(void) { - NotImplemented_nid_no_patch("lPAMjFZEpt8"); - return 0; -} - -APS5_EXPORT("mBagn+lW-iM", sceNpCppWebApiUnknown68); -int APS5_VABI sceNpCppWebApiUnknown68(void) { - NotImplemented_nid_no_patch("mBagn+lW-iM"); - return 0; -} - -APS5_EXPORT("mkWsKEh0h0o", sceNpCppWebApiUnknown69); -int APS5_VABI sceNpCppWebApiUnknown69(void) { - NotImplemented_nid_no_patch("mkWsKEh0h0o"); - return 0; -} - -APS5_EXPORT("nE0ooeCMRm8", sceNpCppWebApiUnknown70); -int APS5_VABI sceNpCppWebApiUnknown70(void) { - NotImplemented_nid_no_patch("nE0ooeCMRm8"); - return 0; -} - -APS5_EXPORT("qf2I9BlKXis", sceNpCppWebApiUnknown71); -int APS5_VABI sceNpCppWebApiUnknown71(void) { - NotImplemented_nid_no_patch("qf2I9BlKXis"); - return 0; -} - -APS5_EXPORT("xwy1I52dE8o", sceNpCppWebApiUnknown72); -int APS5_VABI sceNpCppWebApiUnknown72(void) { - NotImplemented_nid_no_patch("xwy1I52dE8o"); - return 0; -} -} diff --git a/core/libs/prx/libcohtml/CMakeLists.txt b/core/libs/prx/libcohtml/CMakeLists.txt deleted file mode 100644 index 842799d34..000000000 --- a/core/libs/prx/libcohtml/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -add_library(libcohtml SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp -) -target_include_directories(libcohtml PRIVATE ${LIBS_INCLUDE_DIR}) -set_target_properties(libcohtml PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(libcohtml) diff --git a/core/libs/prx/libcohtml/DEPLOY.md b/core/libs/prx/libcohtml/DEPLOY.md deleted file mode 100644 index 0b4cbdc5d..000000000 --- a/core/libs/prx/libcohtml/DEPLOY.md +++ /dev/null @@ -1,16 +0,0 @@ -# libcohtml — deploy note - -Game `DT_NEEDED` expects exactly: `libcohtml.Prospero.prx`. - -Copy next to the relinked eboot (same directory): - -- `build/core/libs/libs/libcohtml.Prospero.prx` (patched build output, 50 NIDs) — required. -- `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, `libwinpthread-1.dll` from `winlibs-gcc15/mingw64/bin` — required on Windows (see [technical debt](../../../../docs/dev/TechnicalDebt.md)). - -Do NOT deploy: - -- `libcohtml.prx` (build intermediate kept for the NID-patch pipeline, wrong `DT_NEEDED` name). -- Anything under `build/core/libs/libs/unpatched/` (NIDs not patched). -- `libc.prx` / `libkernel.prx` are not runtime imports of this module (no such DLL dependency); the game may need its other modules separately (out of scope). - -Scope: raw-NID stubs only; real Cohtml rendering stays out of scope. diff --git a/core/libs/prx/libcohtml/Export.cpp b/core/libs/prx/libcohtml/Export.cpp deleted file mode 100644 index 4ae1996b4..000000000 --- a/core/libs/prx/libcohtml/Export.cpp +++ /dev/null @@ -1,307 +0,0 @@ -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -APS5_EXPORT("1fOA53oGwU8", cohtml_stub_00); -std::uint64_t APS5_VABI cohtml_stub_00(void) { - NotImplemented_nid_no_patch("1fOA53oGwU8"); - return 0; -} - -APS5_EXPORT("2vxXHkZlhbw", cohtml_stub_01); -std::uint64_t APS5_VABI cohtml_stub_01(void) { - NotImplemented_nid_no_patch("2vxXHkZlhbw"); - return 0; -} - -APS5_EXPORT("3rew70M0SmY", cohtml_stub_02); -std::uint64_t APS5_VABI cohtml_stub_02(void) { - NotImplemented_nid_no_patch("3rew70M0SmY"); - return 0; -} - -APS5_EXPORT("4Le0SBPvnF8", cohtml_stub_03); -std::uint64_t APS5_VABI cohtml_stub_03(void) { - NotImplemented_nid_no_patch("4Le0SBPvnF8"); - return 0; -} - -APS5_EXPORT("53ZTBeo2a54", cohtml_stub_04); -std::uint64_t APS5_VABI cohtml_stub_04(void) { - NotImplemented_nid_no_patch("53ZTBeo2a54"); - return 0; -} - -APS5_EXPORT("6PaNuQipllM", cohtml_stub_05); -std::uint64_t APS5_VABI cohtml_stub_05(void) { - NotImplemented_nid_no_patch("6PaNuQipllM"); - return 0; -} - -APS5_EXPORT("7hn5DAs2bLk", cohtml_stub_06); -std::uint64_t APS5_VABI cohtml_stub_06(void) { - NotImplemented_nid_no_patch("7hn5DAs2bLk"); - return 0; -} - -APS5_EXPORT("8EYWdAM2-R0", cohtml_stub_07); -std::uint64_t APS5_VABI cohtml_stub_07(void) { - NotImplemented_nid_no_patch("8EYWdAM2-R0"); - return 0; -} - -APS5_EXPORT("9bGvfp1zbKo", cohtml_stub_08); -std::uint64_t APS5_VABI cohtml_stub_08(void) { - NotImplemented_nid_no_patch("9bGvfp1zbKo"); - return 0; -} - -APS5_EXPORT("a4E5dJh0onE", cohtml_stub_09); -std::uint64_t APS5_VABI cohtml_stub_09(void) { - NotImplemented_nid_no_patch("a4E5dJh0onE"); - return 0; -} - -APS5_EXPORT("BhL-ZtSHmB8", cohtml_stub_10); -std::uint64_t APS5_VABI cohtml_stub_10(void) { - NotImplemented_nid_no_patch("BhL-ZtSHmB8"); - return 0; -} - -APS5_EXPORT("blvwzOvsEAA", cohtml_stub_11); -std::uint64_t APS5_VABI cohtml_stub_11(void) { - NotImplemented_nid_no_patch("blvwzOvsEAA"); - return 0; -} - -APS5_EXPORT("dko-cMqv5yw", cohtml_stub_12); -std::uint64_t APS5_VABI cohtml_stub_12(void) { - NotImplemented_nid_no_patch("dko-cMqv5yw"); - return 0; -} - -APS5_EXPORT("EG4kqkxLU3Q", cohtml_stub_13); -std::uint64_t APS5_VABI cohtml_stub_13(void) { - NotImplemented_nid_no_patch("EG4kqkxLU3Q"); - return 0; -} - -APS5_EXPORT("-El4B0xJF2U", cohtml_stub_14); -std::uint64_t APS5_VABI cohtml_stub_14(void) { - NotImplemented_nid_no_patch("-El4B0xJF2U"); - return 0; -} - -APS5_EXPORT("EMhQNylkSwk", cohtml_stub_15); -std::uint64_t APS5_VABI cohtml_stub_15(void) { - NotImplemented_nid_no_patch("EMhQNylkSwk"); - return 0; -} - -APS5_EXPORT("-eNSKAypRz0", cohtml_stub_16); -std::uint64_t APS5_VABI cohtml_stub_16(void) { - NotImplemented_nid_no_patch("-eNSKAypRz0"); - return 0; -} - -APS5_EXPORT("FG9VRZfson0", cohtml_stub_17); -std::uint64_t APS5_VABI cohtml_stub_17(void) { - NotImplemented_nid_no_patch("FG9VRZfson0"); - return 0; -} - -APS5_EXPORT("fNvtExaHN4U", cohtml_stub_18); -std::uint64_t APS5_VABI cohtml_stub_18(void) { - NotImplemented_nid_no_patch("fNvtExaHN4U"); - return 0; -} - -APS5_EXPORT("g10DkPARoa8", cohtml_stub_19); -std::uint64_t APS5_VABI cohtml_stub_19(void) { - NotImplemented_nid_no_patch("g10DkPARoa8"); - return 0; -} - -APS5_EXPORT("gwQczxoDmG0", cohtml_stub_20); -std::uint64_t APS5_VABI cohtml_stub_20(void) { - NotImplemented_nid_no_patch("gwQczxoDmG0"); - return 0; -} - -APS5_EXPORT("HQF4O9D+Di4", cohtml_stub_21); -std::uint64_t APS5_VABI cohtml_stub_21(void) { - NotImplemented_nid_no_patch("HQF4O9D+Di4"); - return 0; -} - -APS5_EXPORT("igt3a6Q0VMc", cohtml_stub_22); -std::uint64_t APS5_VABI cohtml_stub_22(void) { - NotImplemented_nid_no_patch("igt3a6Q0VMc"); - return 0; -} - -APS5_EXPORT("kyApuhEHHxQ", cohtml_stub_23); -std::uint64_t APS5_VABI cohtml_stub_23(void) { - NotImplemented_nid_no_patch("kyApuhEHHxQ"); - return 0; -} - -APS5_EXPORT("Ljp75QXUi7s", cohtml_stub_24); -std::uint64_t APS5_VABI cohtml_stub_24(void) { - NotImplemented_nid_no_patch("Ljp75QXUi7s"); - return 0; -} - -APS5_EXPORT("loJ5Ef3MZrs", cohtml_stub_25); -std::uint64_t APS5_VABI cohtml_stub_25(void) { - NotImplemented_nid_no_patch("loJ5Ef3MZrs"); - return 0; -} - -APS5_EXPORT("LWe+u8SHKgU", cohtml_stub_26); -std::uint64_t APS5_VABI cohtml_stub_26(void) { - NotImplemented_nid_no_patch("LWe+u8SHKgU"); - return 0; -} - -APS5_EXPORT("nhn1ds+I2t4", cohtml_stub_27); -std::uint64_t APS5_VABI cohtml_stub_27(void) { - NotImplemented_nid_no_patch("nhn1ds+I2t4"); - return 0; -} - -APS5_EXPORT("NmVsRWgsKt4", cohtml_stub_28); -std::uint64_t APS5_VABI cohtml_stub_28(void) { - NotImplemented_nid_no_patch("NmVsRWgsKt4"); - return 0; -} - -APS5_EXPORT("ONEnhMinebs", cohtml_stub_29); -std::uint64_t APS5_VABI cohtml_stub_29(void) { - NotImplemented_nid_no_patch("ONEnhMinebs"); - return 0; -} - -APS5_EXPORT("PdOmR8dy1fM", cohtml_stub_30); -std::uint64_t APS5_VABI cohtml_stub_30(void) { - NotImplemented_nid_no_patch("PdOmR8dy1fM"); - return 0; -} - -APS5_EXPORT("PSwGBN5Icv8", cohtml_stub_31); -std::uint64_t APS5_VABI cohtml_stub_31(void) { - NotImplemented_nid_no_patch("PSwGBN5Icv8"); - return 0; -} - -APS5_EXPORT("RalyfwiTt8o", cohtml_stub_32); -std::uint64_t APS5_VABI cohtml_stub_32(void) { - NotImplemented_nid_no_patch("RalyfwiTt8o"); - return 0; -} - -APS5_EXPORT("t+OHUjVWIwU", cohtml_stub_33); -std::uint64_t APS5_VABI cohtml_stub_33(void) { - NotImplemented_nid_no_patch("t+OHUjVWIwU"); - return 0; -} - -APS5_EXPORT("Ter8YCVZ9tQ", cohtml_stub_34); -std::uint64_t APS5_VABI cohtml_stub_34(void) { - NotImplemented_nid_no_patch("Ter8YCVZ9tQ"); - return 0; -} - -APS5_EXPORT("tG3LJfAOH3k", cohtml_stub_35); -std::uint64_t APS5_VABI cohtml_stub_35(void) { - NotImplemented_nid_no_patch("tG3LJfAOH3k"); - return 0; -} - -APS5_EXPORT("ukSFhKPmCTg", cohtml_stub_36); -std::uint64_t APS5_VABI cohtml_stub_36(void) { - NotImplemented_nid_no_patch("ukSFhKPmCTg"); - return 0; -} - -APS5_EXPORT("V7KGze2XjJc", cohtml_stub_37); -std::uint64_t APS5_VABI cohtml_stub_37(void) { - NotImplemented_nid_no_patch("V7KGze2XjJc"); - return 0; -} - -APS5_EXPORT("vlmAvJqR7mQ", cohtml_stub_38); -std::uint64_t APS5_VABI cohtml_stub_38(void) { - NotImplemented_nid_no_patch("vlmAvJqR7mQ"); - return 0; -} - -APS5_EXPORT("VzhxjXDzYSI", cohtml_stub_39); -std::uint64_t APS5_VABI cohtml_stub_39(void) { - NotImplemented_nid_no_patch("VzhxjXDzYSI"); - return 0; -} - -APS5_EXPORT("X3FzDPzpD2Y", cohtml_stub_40); -std::uint64_t APS5_VABI cohtml_stub_40(void) { - NotImplemented_nid_no_patch("X3FzDPzpD2Y"); - return 0; -} - -APS5_EXPORT("x68HPh+vxsE", cohtml_stub_41); -std::uint64_t APS5_VABI cohtml_stub_41(void) { - NotImplemented_nid_no_patch("x68HPh+vxsE"); - return 0; -} - -APS5_EXPORT("XUe8OAKB-vQ", cohtml_stub_42); -std::uint64_t APS5_VABI cohtml_stub_42(void) { - NotImplemented_nid_no_patch("XUe8OAKB-vQ"); - return 0; -} - -APS5_EXPORT("y6UNQtnr0wY", cohtml_stub_43); -std::uint64_t APS5_VABI cohtml_stub_43(void) { - NotImplemented_nid_no_patch("y6UNQtnr0wY"); - return 0; -} - -APS5_EXPORT("YoHAXkBV06c", cohtml_stub_44); -std::uint64_t APS5_VABI cohtml_stub_44(void) { - NotImplemented_nid_no_patch("YoHAXkBV06c"); - return 0; -} - -APS5_EXPORT("Z9XFJCfmId4", cohtml_stub_45); -std::uint64_t APS5_VABI cohtml_stub_45(void) { - NotImplemented_nid_no_patch("Z9XFJCfmId4"); - return 0; -} - -APS5_EXPORT("zEniuuzG2W0", cohtml_stub_46); -std::uint64_t APS5_VABI cohtml_stub_46(void) { - NotImplemented_nid_no_patch("zEniuuzG2W0"); - return 0; -} - -APS5_EXPORT("zH9Z2nK1fmA", cohtml_stub_47); -std::uint64_t APS5_VABI cohtml_stub_47(void) { - NotImplemented_nid_no_patch("zH9Z2nK1fmA"); - return 0; -} - -APS5_EXPORT("ZHGgdWeYTo8", cohtml_stub_48); -std::uint64_t APS5_VABI cohtml_stub_48(void) { - NotImplemented_nid_no_patch("ZHGgdWeYTo8"); - return 0; -} - -APS5_EXPORT("ZWftPxZ4E6k", cohtml_stub_49); -std::uint64_t APS5_VABI cohtml_stub_49(void) { - NotImplemented_nid_no_patch("ZWftPxZ4E6k"); - return 0; -} - -} diff --git a/core/libs/prx/libfmod/CMakeLists.txt b/core/libs/prx/libfmod/CMakeLists.txt deleted file mode 100644 index 5d1cb47f7..000000000 --- a/core/libs/prx/libfmod/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -add_library(libfmod SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp -) - -target_include_directories(libfmod PRIVATE - ${LIBS_INCLUDE_DIR} - ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include -) -target_link_libraries(libfmod PRIVATE libkernel libc SDL2-static) -target_compile_options(libfmod PRIVATE -fkeep-inline-functions) -configure_windows_unwind(libfmod) diff --git a/core/libs/prx/libfmod/Export.cpp b/core/libs/prx/libfmod/Export.cpp deleted file mode 100644 index bd7545a64..000000000 --- a/core/libs/prx/libfmod/Export.cpp +++ /dev/null @@ -1,1902 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" -#include "SDL.h" - -// FMOD_MODE bits (subset, from fmod_common.h) — used to detect memory buffers. -constexpr unsigned FMOD_LOOP_OFF = 0x00000001u; -constexpr unsigned FMOD_LOOP_NORMAL = 0x00000002u; -constexpr unsigned FMOD_LOOP_BIDI = 0x00000004u; -constexpr unsigned FMOD_2D_FLAG = 0x00000008u; -constexpr unsigned FMOD_3D_FLAG = 0x00000010u; -constexpr unsigned FMOD_CREATESTREAM_FLAG = 0x00000080u; -constexpr unsigned FMOD_OPENMEMORY_FLAG = 0x00000800u; -constexpr unsigned FMOD_OPENMEMORY_POINT_FLAG = 0x10000000u; -constexpr unsigned FMOD_OPENRAW_FLAG = 0x00001000u; -constexpr unsigned FMOD_NONBLOCKING_FLAG = 0x00010000u; - -enum FMOD_OUTPUTTYPE { - FMOD_OUTPUTTYPE_AUTODETECT = 0, - FMOD_OUTPUTTYPE_UNKNOWN = 1, - FMOD_OUTPUTTYPE_NOSOUND = 2, - FMOD_OUTPUTTYPE_WAVWRITER = 3, - FMOD_OUTPUTTYPE_NOSOUND_NRT = 4, - FMOD_OUTPUTTYPE_WAVWRITER_NRT = 5, - FMOD_OUTPUTTYPE_WASAPI = 6, - FMOD_OUTPUTTYPE_ASIO = 7, - FMOD_OUTPUTTYPE_PULSEAUDIO = 8, - FMOD_OUTPUTTYPE_ALSA = 9, - FMOD_OUTPUTTYPE_COREAUDIO = 10, - FMOD_OUTPUTTYPE_AUDIOTRACK = 11, - FMOD_OUTPUTTYPE_OPENSL = 12, - FMOD_OUTPUTTYPE_AUDIOOUT = 13, - FMOD_OUTPUTTYPE_AUDIO3D = 14, - FMOD_OUTPUTTYPE_MAX = 15 -}; - -enum FMOD_SPEAKERMODE { - FMOD_SPEAKERMODE_DEFAULT = 0, - FMOD_SPEAKERMODE_RAW = 1, - FMOD_SPEAKERMODE_MONO = 2, - FMOD_SPEAKERMODE_STEREO = 3, - FMOD_SPEAKERMODE_QUAD = 4, - FMOD_SPEAKERMODE_SURROUND = 5, - FMOD_SPEAKERMODE_5POINT1 = 6, - FMOD_SPEAKERMODE_7POINT1 = 7, - FMOD_SPEAKERMODE_7POINT1POINT4 = 8, - FMOD_SPEAKERMODE_MAX = 9 -}; - -enum FMOD_SOUND_TYPE { - FMOD_SOUND_TYPE_UNKNOWN = 0, - FMOD_SOUND_TYPE_AIFF = 1, - FMOD_SOUND_TYPE_ASF = 2, - FMOD_SOUND_TYPE_DLS = 3, - FMOD_SOUND_TYPE_FLAC = 4, - FMOD_SOUND_TYPE_FSB = 5, - FMOD_SOUND_TYPE_IT = 6, - FMOD_SOUND_TYPE_MIDI = 7, - FMOD_SOUND_TYPE_MOD = 8, - FMOD_SOUND_TYPE_MPEG = 9, - FMOD_SOUND_TYPE_OGGVORBIS = 10, - FMOD_SOUND_TYPE_PLAYLIST = 11, - FMOD_SOUND_TYPE_RAW = 12, - FMOD_SOUND_TYPE_S3M = 13, - FMOD_SOUND_TYPE_USER = 14, - FMOD_SOUND_TYPE_XM = 15, - FMOD_SOUND_TYPE_XMA = 16, - FMOD_SOUND_TYPE_AUDIOQUEUE = 17, - FMOD_SOUND_TYPE_AT9 = 18, - FMOD_SOUND_TYPE_VB = 19, - FMOD_SOUND_TYPE_MAX = 20 -}; - -enum FMOD_SOUND_FORMAT { - FMOD_SOUND_FORMAT_NONE = 0, - FMOD_SOUND_FORMAT_PCM8 = 1, - FMOD_SOUND_FORMAT_PCM16 = 2, - FMOD_SOUND_FORMAT_PCM24 = 3, - FMOD_SOUND_FORMAT_PCM32 = 4, - FMOD_SOUND_FORMAT_PCMFLOAT = 5, - FMOD_SOUND_FORMAT_BITSTREAM = 6, - FMOD_SOUND_FORMAT_MAX = 7 -}; - -enum FMOD_OPENSTATE { - FMOD_OPENSTATE_READY = 0, - FMOD_OPENSTATE_LOADING = 1, - FMOD_OPENSTATE_ERROR = 2, - FMOD_OPENSTATE_CONNECTING = 3, - FMOD_OPENSTATE_BUFFERING = 4, - FMOD_OPENSTATE_SEEKING = 5, - FMOD_OPENSTATE_PLAYING = 6, - FMOD_OPENSTATE_SETPOSITION = 7, - FMOD_OPENSTATE_MAX = 8 -}; - -enum FMOD_SOUNDGROUP_BEHAVIOR { - FMOD_SOUNDGROUP_BEHAVIOR_FAIL = 0, - FMOD_SOUNDGROUP_BEHAVIOR_MUTE = 1, - FMOD_SOUNDGROUP_BEHAVIOR_STEALLOWEST = 2, - FMOD_SOUNDGROUP_BEHAVIOR_MAX = 3 -}; - -enum FMOD_DSP_TYPE { - FMOD_DSP_TYPE_UNKNOWN = 0, - FMOD_DSP_TYPE_MIXER = 1, - FMOD_DSP_TYPE_OSCILLATOR = 2, - FMOD_DSP_TYPE_LOWPASS = 3, - FMOD_DSP_TYPE_ITLOWPASS = 4, - FMOD_DSP_TYPE_HIGHPASS = 5, - FMOD_DSP_TYPE_ECHO = 6, - FMOD_DSP_TYPE_FADER = 7, - FMOD_DSP_TYPE_FLANGE = 8, - FMOD_DSP_TYPE_DISTORTION = 9, - FMOD_DSP_TYPE_NORMALIZE = 10, - FMOD_DSP_TYPE_LIMITER = 11, - FMOD_DSP_TYPE_PARAMEQ = 12, - FMOD_DSP_TYPE_PITCHSHIFT = 13, - FMOD_DSP_TYPE_CHORUS = 14, - FMOD_DSP_TYPE_VSTPLUGIN = 15, - FMOD_DSP_TYPE_WINAMPPLUGIN = 16, - FMOD_DSP_TYPE_ITECHO = 17, - FMOD_DSP_TYPE_COMPRESSOR = 18, - FMOD_DSP_TYPE_SFXREVERB = 19, - FMOD_DSP_TYPE_LOWPASS_SIMPLE = 20, - FMOD_DSP_TYPE_DELAY = 21, - FMOD_DSP_TYPE_TREMOLO = 22, - FMOD_DSP_TYPE_SEND = 24, - FMOD_DSP_TYPE_RETURN = 25, - FMOD_DSP_TYPE_HIGHPASS_SIMPLE = 26, - FMOD_DSP_TYPE_PAN = 27, - FMOD_DSP_TYPE_THREE_EQ = 28, - FMOD_DSP_TYPE_FFT = 29, - FMOD_DSP_TYPE_LOUDNESS_METER = 30, - FMOD_DSP_TYPE_ENVELOPEFOLLOWER = 31, - FMOD_DSP_TYPE_CONVOLUTIONREVERB = 32, - FMOD_DSP_TYPE_TRANSCEIVER = 33, - FMOD_DSP_TYPE_MAX = 34 -}; - -enum FMOD_DSPCONNECTION_TYPE { - FMOD_DSPCONNECTION_TYPE_STANDARD = 0, - FMOD_DSPCONNECTION_TYPE_SIDECHAIN = 1, - FMOD_DSPCONNECTION_TYPE_SEND = 2, - FMOD_DSPCONNECTION_TYPE_SEND_SIDECHAIN = 3, - FMOD_DSPCONNECTION_TYPE_MAX = 4 -}; - -enum FMOD_DSP_FFT_WINDOW { - FMOD_DSP_FFT_WINDOW_RECT = 0, - FMOD_DSP_FFT_WINDOW_TRIANGLE = 1, - FMOD_DSP_FFT_WINDOW_HAMMING = 2, - FMOD_DSP_FFT_WINDOW_HANNING = 3, - FMOD_DSP_FFT_WINDOW_BLACKMAN = 4, - FMOD_DSP_FFT_WINDOW_BLACKMANHARRIS = 5, - FMOD_DSP_FFT_WINDOW_MAX = 6 -}; - -enum FMOD_PLUGINTYPE { - FMOD_PLUGINTYPE_OUTPUT = 0, - FMOD_PLUGINTYPE_CODEC = 1, - FMOD_PLUGINTYPE_DSP = 2, - FMOD_PLUGINTYPE_MAX = 3 -}; - -enum FMOD_DRIVER_STATE { - FMOD_DRIVER_STATE_CONNECTED = 1, - FMOD_DRIVER_STATE_DEFAULT = 2 -}; - -enum FMOD_PORT_TYPE { - FMOD_PORT_TYPE_MUSIC = 0, - FMOD_PORT_TYPE_COPY = 1, - FMOD_PORT_TYPE_SPLIT = 2, - FMOD_PORT_TYPE_FEEDBACK = 3, - FMOD_PORT_TYPE_MAX = 4 -}; - -enum FMOD_SYSTEM_CALLBACK_TYPE { - FMOD_SYSTEM_CALLBACK_DEVICELISTCHANGED = 1, - FMOD_SYSTEM_CALLBACK_DEVICELOST = 2, - FMOD_SYSTEM_CALLBACK_MEMORYALLOCATIONFAILED = 4, - FMOD_SYSTEM_CALLBACK_THREADCREATED = 8, - FMOD_SYSTEM_CALLBACK_BADDSPCONNECTION = 16, - FMOD_SYSTEM_CALLBACK_PREMIX = 32, - FMOD_SYSTEM_CALLBACK_POSTMIX = 64, - FMOD_SYSTEM_CALLBACK_ERROR = 128, - FMOD_SYSTEM_CALLBACK_MIDMIX = 256, - FMOD_SYSTEM_CALLBACK_THREADDESTROYED = 512, - FMOD_SYSTEM_CALLBACK_PREUPDATE = 1024, - FMOD_SYSTEM_CALLBACK_POSTUPDATE = 2048, - FMOD_SYSTEM_CALLBACK_ALL = 0xFFFFFFFF -}; - -enum FMOD_CHANNELCONTROL_TYPE { - FMOD_CHANNELCONTROL_CHANNEL = 0, - FMOD_CHANNELCONTROL_CHANNELGROUP = 1, - FMOD_CHANNELCONTROL_MAX = 2 -}; - -enum FMOD_CHANNELCONTROL_CALLBACK_TYPE { - FMOD_CHANNELCONTROL_CALLBACK_END = 0, - FMOD_CHANNELCONTROL_CALLBACK_VIRTUALVOICE = 1, - FMOD_CHANNELCONTROL_CALLBACK_SYNCPOINT = 2, - FMOD_CHANNELCONTROL_CALLBACK_OCCLUSION = 3, - FMOD_CHANNELCONTROL_CALLBACK_MAX = 4 -}; - -enum FMOD_TAGTYPE { - FMOD_TAGTYPE_UNKNOWN = 0, - FMOD_TAGTYPE_ID3V1 = 1, - FMOD_TAGTYPE_ID3V2 = 2, - FMOD_TAGTYPE_VORBISCOMMENT = 3, - FMOD_TAGTYPE_SHOUTCAST = 4, - FMOD_TAGTYPE_ICECAST = 5, - FMOD_TAGTYPE_ASF = 6, - FMOD_TAGTYPE_MIDI = 7, - FMOD_TAGTYPE_PLAYLIST = 8, - FMOD_TAGTYPE_FMOD = 9, - FMOD_TAGTYPE_USER = 10, - FMOD_TAGTYPE_MAX = 11 -}; - -enum FMOD_TAGDATATYPE { - FMOD_TAGDATATYPE_BINARY = 0, - FMOD_TAGDATATYPE_INT = 1, - FMOD_TAGDATATYPE_FLOAT = 2, - FMOD_TAGDATATYPE_STRING = 3, - FMOD_TAGDATATYPE_STRING_UTF16 = 4, - FMOD_TAGDATATYPE_STRING_UTF16BE = 5, - FMOD_TAGDATATYPE_STRING_UTF8 = 6, - FMOD_TAGDATATYPE_CDTOC = 7, - FMOD_TAGDATATYPE_MAX = 8 -}; - -struct FMOD_GUID { - unsigned Data1; - unsigned short Data2; - unsigned short Data3; - unsigned char Data4[8]; -}; - -struct FMOD_VECTOR { - float x; - float y; - float z; -}; - -struct FMOD_3D_ATTRIBUTES { - FMOD_VECTOR position; - FMOD_VECTOR velocity; - FMOD_VECTOR forward; - FMOD_VECTOR up; -}; - -struct FMOD_CREATESOUNDEXINFO { - int cbsize; - unsigned length; - unsigned fileoffset; - int numchannels; - int defaultfrequency; - FMOD_SOUND_FORMAT format; - unsigned decodebuffersize; - int initialsubsound; - int numsubsounds; - void* inclusionlist; - unsigned inclusionlistnum; - unsigned char dlsrawdata[8]; - char* encryptionkey; -}; - -struct FMOD_REVERB_PROPERTIES { - int Instance; - float Environment; - float EnvSize; - float EnvDiffusion; - int Room; - int RoomHF; - int RoomLF; - float DecayTime; - float DecayHFRatio; - float DecayLFRatio; - int Reflections; - float ReflectionsDelay; - int Reverb; - float ReverbDelay; - float EchoTime; - float EchoDepth; - float ModulationTime; - float ModulationDepth; - float AirAbsorptionHF; - float HFReference; - float LFReference; - float RoomRolloffFactor; - float Diffusion; - float Density; - unsigned Flags; -}; - -struct FMOD_ADVANCEDSETTINGS { - int cbSize; - unsigned maxMPEGCodecs; - unsigned maxADPCMCodecs; - unsigned maxXMACodecs; - unsigned maxVorbisCodecs; - unsigned maxAT9Codecs; - unsigned maxFADPCMCodecs; - unsigned maxPCMCodecs; - unsigned ASIONumChannels; - char* ASIOChannelList; - char* ASIOSpeakerList; - float vol0virtualvol; - unsigned defaultDecodeBufferSize; - unsigned short profilePort; - unsigned geometryMaxFadeTime; - float distanceFilterCenterFreq; - int reverb3Dinstance; - unsigned DSPBufferPoolSize; - unsigned stackSizeStream; - unsigned stackSizeNonBlocking; - unsigned stackSizeMixer; - int resamplerMethod; - unsigned commandQueueSize; - unsigned handleInitialSize; -}; - -struct FMOD_CPU_USAGE { - float dsp; - float stream; - float geometry; - float update; - float convolution1; - float convolution2; -}; - -struct FMOD_TAG { - FMOD_TAGTYPE type; - FMOD_TAGDATATYPE datatype; - char* name; - void* data; - unsigned datalen; - bool updated; -}; - -struct FMOD_SYNC_POINT { - void* opaque; -}; - -struct FMOD_DSP_DESCRIPTION { - unsigned pluginsdkversion; - char name[32]; - unsigned version; - int numinputbuffers; - int numoutputbuffers; - void* create; - void* release; - void* reset; - void* read; - void* process; - void* setposition; - int numparameters; - void* paramdesc; - void* setparameterfloat; - void* setparameterint; - void* setparameterbool; - void* setparameterdata; - void* getparameterfloat; - void* getparameterint; - void* getparameterbool; - void* getparameterdata; - void* shouldiprocess; - void* userdata; - void* sys_register; - void* sys_deregister; - void* sys_mix; -}; - -struct FMOD_DSP_PARAMETER_DESC { - int type; - char name[16]; - char label[16]; - char* description; -}; - -struct FMOD_DSP_METERING_INFO { - int numsamples; - float peaklevel[32]; - float rmslevel[32]; - short numchannels; -}; - -struct FMOD_DSP_BUFFER_ARRAY { - int numbuffers; - int* buffernumchannels; - int* bufferchannelmask; - float** buffers; - FMOD_SPEAKERMODE speakermode; -}; - -struct FMOD_DSP_STATE { - void* instance; - void* plugindata; - int channelmask; - FMOD_SPEAKERMODE source_speakermode; - float* sidechaindata; - int sidechainchannels; - void* callbacks; - int systemobject; -}; - -struct FMOD_SYSTEM { - int unused; -}; - -struct FMOD_CHANNELCONTROL { - int unused; -}; - -struct FMOD_CHANNELGROUP { - int unused; -}; - -typedef int (*FMOD_SYSTEM_CALLBACK)(FMOD_SYSTEM* system, FMOD_SYSTEM_CALLBACK_TYPE type, void* commanddata1, void* commanddata2, void* userdata); -typedef int (*FMOD_CHANNELCONTROL_CALLBACK)(FMOD_CHANNELCONTROL* channelcontrol, FMOD_CHANNELCONTROL_TYPE controltype, FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, void* commanddata1, void* commanddata2); - -namespace { - -std::mutex gFmodMutex; - -constexpr int FMOD_OK = 0; -constexpr unsigned FMOD_VERSION_CURRENT = 0x00020206; - -// ---- Real-audio backend (SDL queue, mirrors libSceAudioOut) ---- -// Silent fallback is kept via ANYPS5_FMOD_SILENT=1 or NOSOUND output type -// or when SDL cannot be opened. When silent, update() is a no-op and -// isPlaying() keeps the previous sticky behaviour so the old smoke test -// (playSound(nullptr) -> playing==true) still passes. -constexpr int kFmodOutRate = 48000; -constexpr int kFmodOutCh = 2; -constexpr int kFmodMixFrames = 1024; - -static SDL_AudioDeviceID gFmodDevice = 0; -static SDL_AudioSpec gFmodSpec = {}; -static bool gFmodSdlTried = false; -static bool gFmodSilent = false; - -static bool FmodSilentRequested() { - const char* s = std::getenv("ANYPS5_FMOD_SILENT"); - if (s && (s[0] == '1' || s[0] == 'y' || s[0] == 'Y')) return true; - s = std::getenv("ANYPS5_FMOD_NOSOUND"); - if (s && (s[0] == '1' || s[0] == 'y' || s[0] == 'Y')) return true; - return false; -} - -static bool FmodEnsureDevice() { - if (gFmodDevice != 0) return true; - if (gFmodSdlTried) return gFmodDevice != 0; - gFmodSdlTried = true; - if (FmodSilentRequested()) { - gFmodSilent = true; - return false; - } - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { - gFmodSilent = true; - return false; - } - SDL_AudioSpec desired{}; - desired.freq = kFmodOutRate; - desired.format = AUDIO_F32SYS; - desired.channels = static_cast(kFmodOutCh); - desired.samples = 1024; - desired.callback = nullptr; - SDL_AudioSpec obtained{}; - gFmodDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE); - if (gFmodDevice == 0) { - gFmodSilent = true; - return false; - } - gFmodSpec = obtained; - SDL_PauseAudioDevice(gFmodDevice, 0); - return true; -} - -static void FmodMakeSine(std::vector& out, int rate, int ch, float freqHz, float seconds) { - const size_t frames = static_cast(rate * seconds); - out.resize(frames * static_cast(ch)); - const float phaseInc = 2.0f * 3.14159265358979323846f * freqHz / static_cast(rate); - float phase = 0.0f; - for (size_t f = 0; f < frames; f++) { - const float s = std::sin(phase) * 0.25f; - phase += phaseInc; - for (int c = 0; c < ch; c++) out[f * static_cast(ch) + static_cast(c)] = s; - } -} - -// Minimal WAV (RIFF/WAVE) parser -> float stereo 48k. Returns false if not WAV. -static bool FmodParseWav(const uint8_t* data, size_t len, std::vector& outPcm, int& outRate, int& outCh) { - if (len < 44 || std::memcmp(data, "RIFF", 4) != 0 || std::memcmp(data + 8, "WAVE", 4) != 0) return false; - uint16_t audioFmt = 0, numCh = 0, bitsPerSample = 0; - uint32_t sampleRate = 0; - const uint8_t* snd = nullptr; - size_t sndLen = 0; - size_t off = 12; - auto rd16 = [](const uint8_t* p) -> uint16_t { return static_cast(p[0] | (p[1] << 8)); }; - auto rd32 = [](const uint8_t* p) -> uint32_t { return static_cast(p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24)); }; - while (off + 8 <= len) { - const uint8_t* chunk = data + off; - uint32_t chunkLen = rd32(chunk + 4); - if (off + 8 + chunkLen > len) break; - if (std::memcmp(chunk, "fmt ", 4) == 0 && chunkLen >= 16) { - audioFmt = rd16(chunk + 8); - numCh = rd16(chunk + 10); - sampleRate = rd32(chunk + 12); - bitsPerSample = rd16(chunk + 22); - } else if (std::memcmp(chunk, "data", 4) == 0) { - snd = chunk + 8; - sndLen = chunkLen; - } - off += 8 + ((chunkLen + 1) & ~static_cast(1)); - } - if (!snd || sndLen == 0 || numCh == 0 || sampleRate == 0) return false; - if (audioFmt != 1 && audioFmt != 3) return false; // PCM or FLOAT - int bytesPerSamp = bitsPerSample / 8; - if (bytesPerSamp <= 0) return false; - size_t frames = sndLen / (static_cast(numCh) * static_cast(bytesPerSamp)); - if (frames == 0) return false; - if (frames > 48000u * 30u) frames = 48000u * 30u; // cap 30s - std::vector tmp; - tmp.reserve(frames * 2); - for (size_t f = 0; f < frames; f++) { - float l = 0.0f, r = 0.0f; - for (int c = 0; c < numCh && c < 8; c++) { - const uint8_t* p = snd + (f * numCh + c) * bytesPerSamp; - float s = 0.0f; - if (audioFmt == 1) { - if (bitsPerSample == 8) s = (static_cast(p[0]) - 128) / 128.0f; - else if (bitsPerSample == 16) { int16_t v; std::memcpy(&v, p, 2); s = v / 32768.0f; } - else if (bitsPerSample == 24) { int32_t v = (p[0] | (p[1] << 8) | (p[2] << 16)); if (v & 0x800000) v |= ~0xFFFFFF; s = v / 8388608.0f; } - else if (bitsPerSample == 32) { int32_t v; std::memcpy(&v, p, 4); s = v / 2147483648.0f; } - else return false; - } else { - if (bitsPerSample == 32) { float v; std::memcpy(&v, p, 4); s = v; } - else return false; - } - if (c == 0) l = s; - else if (c == 1) r = s; - else { l += s; r += s; } - } - if (numCh == 1) r = l; - tmp.push_back(l); - tmp.push_back(r); - } - // Resample to 48k if needed (linear). - if (sampleRate != static_cast(kFmodOutRate)) { - const size_t inFrames = tmp.size() / 2; - const size_t outFrames = static_cast((static_cast(inFrames) * kFmodOutRate) / sampleRate); - std::vector rs; - rs.reserve(outFrames * 2); - for (size_t i = 0; i < outFrames; i++) { - double pos = (static_cast(i) * sampleRate) / kFmodOutRate; - size_t i0 = static_cast(pos); - size_t i1 = i0 + 1 < inFrames ? i0 + 1 : i0; - float t = static_cast(pos - i0); - rs.push_back(tmp[i0 * 2] * (1 - t) + tmp[i1 * 2] * t); - rs.push_back(tmp[i0 * 2 + 1] * (1 - t) + tmp[i1 * 2 + 1] * t); - } - outPcm.swap(rs); - } else { - outPcm.swap(tmp); - } - outRate = kFmodOutRate; - outCh = 2; - return true; -} - -} - -namespace FMOD { - -class System; -class ChannelGroup; -class Channel; -class Sound; -class SoundGroup; -class DSP; -class DSPConnection; -class Geometry; -class Reverb3D; - -class Sound { -public: - std::string name; - unsigned mode = 0; - unsigned length = 0; - int loopCount = 0; - float minDistance = 0.0f; - float maxDistance = 10000.0f; - SoundGroup* soundGroup = nullptr; - void* userData = nullptr; - // Real-audio decoded PCM (float stereo 48k, interleaved). Empty = silent/placeholder. - std::vector pcm; - int pcmRate = kFmodOutRate; - int pcmCh = 2; - int APS5_VABI release(); - int APS5_VABI getSystemObject(System** system); - int APS5_VABI setMode(unsigned mode) { - std::lock_guard lock(gFmodMutex); - this->mode = mode; - return FMOD_OK; - } - int APS5_VABI getMode(unsigned* mode) { - std::lock_guard lock(gFmodMutex); - if (mode) *mode = this->mode; - return FMOD_OK; - } - int APS5_VABI set3DMinMaxDistance(float mindistance, float maxdistance) { - std::lock_guard lock(gFmodMutex); - minDistance = mindistance; - maxDistance = maxdistance; - return FMOD_OK; - } - int APS5_VABI getNumSubSounds(int* numsubsounds) { - std::lock_guard lock(gFmodMutex); - if (numsubsounds) *numsubsounds = 0; - return FMOD_OK; - } - int APS5_VABI getLength(unsigned* length, unsigned lengthtype) { - std::lock_guard lock(gFmodMutex); - if (!length) return FMOD_OK; - if (!pcm.empty()) { - const unsigned frames = static_cast(pcm.size() / 2); - // FMOD_TIMEUNIT: MS=1, PCM=2, PCMBYTES=4, RAWBYTES=8 - if (lengthtype == 1) *length = (frames * 1000u) / static_cast(kFmodOutRate); - else if (lengthtype == 2) *length = frames; - else if (lengthtype == 4 || lengthtype == 8) *length = frames * 2u * sizeof(float); - else *length = frames; - return FMOD_OK; - } - *length = this->length; - return FMOD_OK; - } - int APS5_VABI getFormat(FMOD_SOUND_TYPE* type, FMOD_SOUND_FORMAT* format, int* channels, int* bits) { - std::lock_guard lock(gFmodMutex); - if (!pcm.empty()) { - if (type) *type = FMOD_SOUND_TYPE_UNKNOWN; - if (format) *format = FMOD_SOUND_FORMAT_PCMFLOAT; - if (channels) *channels = 2; - if (bits) *bits = 32; - return FMOD_OK; - } - if (type) *type = FMOD_SOUND_TYPE_UNKNOWN; - if (format) *format = FMOD_SOUND_FORMAT_PCM16; - if (channels) *channels = 2; - if (bits) *bits = 16; - return FMOD_OK; - } - int APS5_VABI getName(char* name, int namelen) { - std::lock_guard lock(gFmodMutex); - if (name && namelen > 0) { - std::strncpy(name, this->name.c_str(), static_cast(namelen - 1)); - name[namelen - 1] = '\0'; - } - return FMOD_OK; - } - int APS5_VABI setLoopCount(int loopcount) { - std::lock_guard lock(gFmodMutex); - loopCount = loopcount; - return FMOD_OK; - } - int APS5_VABI getLoopCount(int* loopcount) { - std::lock_guard lock(gFmodMutex); - if (loopcount) *loopcount = loopCount; - return FMOD_OK; - } - int APS5_VABI setSoundGroup(SoundGroup* soundgroup) { - std::lock_guard lock(gFmodMutex); - soundGroup = soundgroup; - return FMOD_OK; - } - int APS5_VABI getSoundGroup(SoundGroup** soundgroup) { - std::lock_guard lock(gFmodMutex); - if (soundgroup) *soundgroup = soundGroup; - return FMOD_OK; - } - int APS5_VABI getSubSound(int index, Sound** subsound) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (subsound) *subsound = nullptr; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI getOpenState(FMOD_OPENSTATE* openstate, unsigned* percentbuffered, bool* starving, bool* diskbusy) { - std::lock_guard lock(gFmodMutex); - if (openstate) *openstate = FMOD_OPENSTATE_READY; - if (percentbuffered) *percentbuffered = 100; - if (starving) *starving = false; - if (diskbusy) *diskbusy = false; - return FMOD_OK; - } - int APS5_VABI readData(void* buffer, unsigned length, unsigned* read) { - std::lock_guard lock(gFmodMutex); - if (buffer && length) std::memset(buffer, 0, length); - if (read) *read = length; - return FMOD_OK; - } - int APS5_VABI seekData(unsigned pcm) { - (void)pcm; - return FMOD_OK; - } -}; - -class SoundGroup { -public: - int maxAudible = 0; - float volume = 1.0f; - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI setMaxAudible(int maxaudible) { - std::lock_guard lock(gFmodMutex); - maxAudible = maxaudible; - return FMOD_OK; - } - int APS5_VABI getMaxAudible(int* maxaudible) { - std::lock_guard lock(gFmodMutex); - if (maxaudible) *maxaudible = maxAudible; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gFmodMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getVolume(float* volume) { - std::lock_guard lock(gFmodMutex); - if (volume) *volume = this->volume; - return FMOD_OK; - } - int APS5_VABI stop() { return FMOD_OK; } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI getNumSounds(int* numsounds) { - std::lock_guard lock(gFmodMutex); - if (numsounds) *numsounds = 0; - return FMOD_OK; - } -}; - -class DSPConnection { -public: - DSP* input = nullptr; - DSP* output = nullptr; - void* userData = nullptr; - int APS5_VABI getInput(DSP** input) { - std::lock_guard lock(gFmodMutex); - if (input) *input = this->input; - return FMOD_OK; - } - int APS5_VABI getOutput(DSP** output) { - std::lock_guard lock(gFmodMutex); - if (output) *output = this->output; - return FMOD_OK; - } - int APS5_VABI getType(FMOD_DSPCONNECTION_TYPE* type) { - std::lock_guard lock(gFmodMutex); - if (type) *type = FMOD_DSPCONNECTION_TYPE_STANDARD; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class DSP { -public: - FMOD_DSP_TYPE type = FMOD_DSP_TYPE_UNKNOWN; - bool active = true; - bool bypass = false; - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI setActive(bool active) { - std::lock_guard lock(gFmodMutex); - this->active = active; - return FMOD_OK; - } - int APS5_VABI getActive(bool* active) { - std::lock_guard lock(gFmodMutex); - if (active) *active = this->active; - return FMOD_OK; - } - int APS5_VABI setBypass(bool bypass) { - std::lock_guard lock(gFmodMutex); - this->bypass = bypass; - return FMOD_OK; - } - int APS5_VABI getBypass(bool* bypass) { - std::lock_guard lock(gFmodMutex); - if (bypass) *bypass = this->bypass; - return FMOD_OK; - } - int APS5_VABI reset() { return FMOD_OK; } - int APS5_VABI setParameterFloat(int index, float value) { - (void)index; (void)value; - return FMOD_OK; - } - int APS5_VABI getParameterFloat(int index, float* value) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (value) *value = 0.0f; - return FMOD_OK; - } - int APS5_VABI getType(FMOD_DSP_TYPE* type) { - std::lock_guard lock(gFmodMutex); - if (type) *type = this->type; - return FMOD_OK; - } - int APS5_VABI getInfo(char* name, unsigned* version, int* channels, int* configwidth, int* configheight) { - std::lock_guard lock(gFmodMutex); - if (name) name[0] = '\0'; - if (version) *version = 0; - if (channels) *channels = 0; - if (configwidth) *configwidth = 0; - if (configheight) *configheight = 0; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class ChannelControl { -public: - float volume = 1.0f; - float pitch = 1.0f; - bool paused = false; - bool mute = false; - bool playing = false; - unsigned mode = 0; - bool volumeRamp = false; - void* userData = nullptr; - // Mixer state for real-audio backend (used when this control is a Channel - // with decoded PCM; groups keep mixHasData==false and use sticky flag). - size_t mixCursor = 0; - size_t mixTotal = 0; - bool mixHasData = false; - bool mixLoop = false; - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gFmodMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getVolume(float* volume) { - std::lock_guard lock(gFmodMutex); - if (volume) *volume = this->volume; - return FMOD_OK; - } - int APS5_VABI setPitch(float pitch) { - std::lock_guard lock(gFmodMutex); - this->pitch = pitch; - return FMOD_OK; - } - int APS5_VABI getPitch(float* pitch) { - std::lock_guard lock(gFmodMutex); - if (pitch) *pitch = this->pitch; - return FMOD_OK; - } - int APS5_VABI setPaused(bool paused) { - std::lock_guard lock(gFmodMutex); - this->paused = paused; - return FMOD_OK; - } - int APS5_VABI getPaused(bool* paused) { - std::lock_guard lock(gFmodMutex); - if (paused) *paused = this->paused; - return FMOD_OK; - } - int APS5_VABI setMute(bool mute) { - std::lock_guard lock(gFmodMutex); - this->mute = mute; - return FMOD_OK; - } - int APS5_VABI getMute(bool* mute) { - std::lock_guard lock(gFmodMutex); - if (mute) *mute = this->mute; - return FMOD_OK; - } - int APS5_VABI stop() { - std::lock_guard lock(gFmodMutex); - playing = false; - return FMOD_OK; - } - int APS5_VABI isPlaying(bool* isplaying) { - std::lock_guard lock(gFmodMutex); - if (!isplaying) return FMOD_OK; - if (mixHasData) { - *isplaying = playing && !paused && (mixLoop || mixCursor < mixTotal); - } else { - *isplaying = playing && !paused; - } - return FMOD_OK; - } - int APS5_VABI setMode(unsigned mode) { - std::lock_guard lock(gFmodMutex); - this->mode = mode; - return FMOD_OK; - } - int APS5_VABI getMode(unsigned* mode) { - std::lock_guard lock(gFmodMutex); - if (mode) *mode = this->mode; - return FMOD_OK; - } - int APS5_VABI addFadePoint(unsigned long long dspclock, float volume) { - (void)dspclock; (void)volume; - return FMOD_OK; - } - int APS5_VABI setDelay(unsigned long long dspclock_start, unsigned long long dspclock_end, bool stopchannels) { - (void)dspclock_start; (void)dspclock_end; (void)stopchannels; - return FMOD_OK; - } - int APS5_VABI getDelay(unsigned long long* dspclock_start, unsigned long long* dspclock_end, bool* stopchannels) { - std::lock_guard lock(gFmodMutex); - if (dspclock_start) *dspclock_start = 0; - if (dspclock_end) *dspclock_end = 0; - if (stopchannels) *stopchannels = false; - return FMOD_OK; - } - int APS5_VABI setVolumeRamp(bool ramp) { - std::lock_guard lock(gFmodMutex); - volumeRamp = ramp; - return FMOD_OK; - } - int APS5_VABI getVolumeRamp(bool* ramp) { - std::lock_guard lock(gFmodMutex); - if (ramp) *ramp = volumeRamp; - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_CHANNELCONTROL_CALLBACK callback) { - (void)callback; - return FMOD_OK; - } - int APS5_VABI isVirtual(bool* isvirtual) { - std::lock_guard lock(gFmodMutex); - if (isvirtual) *isvirtual = false; - return FMOD_OK; - } - int APS5_VABI getIndex(int* index) { - std::lock_guard lock(gFmodMutex); - if (index) *index = 0; - return FMOD_OK; - } - int APS5_VABI getSystemObject(System** system); - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI set3DAttributes(const FMOD_VECTOR* pos, const FMOD_VECTOR* vel) { - (void)pos; (void)vel; - return FMOD_OK; - } - int APS5_VABI setLowPassGain(float gain) { - (void)gain; - return FMOD_OK; - } - int APS5_VABI getLowPassGain(float* gain) { - std::lock_guard lock(gFmodMutex); - if (gain) *gain = 1.0f; - return FMOD_OK; - } - int APS5_VABI setPan(float pan) { - (void)pan; - return FMOD_OK; - } - int APS5_VABI getDSPClock(unsigned long long* dspclock, unsigned long long* parentclock) { - std::lock_guard lock(gFmodMutex); - if (dspclock) *dspclock = 0; - if (parentclock) *parentclock = 0; - return FMOD_OK; - } - int APS5_VABI getDSP(int index, DSP** dsp) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (dsp) *dsp = nullptr; - return FMOD_OK; - } -}; - -class Channel : public ChannelControl { -public: - Sound* sound = nullptr; - ChannelGroup* group = nullptr; - float frequency = 48000.0f; - int priority = 128; - unsigned position = 0; - int loopCount = 0; - System* owner = nullptr; - int APS5_VABI setPriority(int priority) { - std::lock_guard lock(gFmodMutex); - this->priority = priority; - return FMOD_OK; - } - int APS5_VABI getPriority(int* priority) { - std::lock_guard lock(gFmodMutex); - if (priority) *priority = this->priority; - return FMOD_OK; - } - int APS5_VABI setFrequency(float frequency) { - std::lock_guard lock(gFmodMutex); - this->frequency = frequency; - return FMOD_OK; - } - int APS5_VABI getFrequency(float* frequency) { - std::lock_guard lock(gFmodMutex); - if (frequency) *frequency = this->frequency; - return FMOD_OK; - } - int APS5_VABI setPosition(unsigned position, unsigned postype) { - (void)postype; - std::lock_guard lock(gFmodMutex); - this->position = position; - // Keep mixer cursor in sync (PCM frames). postype==2 (PCM) assumed. - mixCursor = static_cast(position); - if (mixHasData && mixCursor > mixTotal) mixCursor = mixTotal; - return FMOD_OK; - } - int APS5_VABI getPosition(unsigned* position, unsigned postype) { - (void)postype; - std::lock_guard lock(gFmodMutex); - if (position) *position = static_cast(mixHasData ? mixCursor : this->position); - return FMOD_OK; - } - int APS5_VABI setChannelGroup(ChannelGroup* channelgroup) { - std::lock_guard lock(gFmodMutex); - group = channelgroup; - return FMOD_OK; - } - int APS5_VABI getChannelGroup(ChannelGroup** channelgroup) { - std::lock_guard lock(gFmodMutex); - if (channelgroup) *channelgroup = group; - return FMOD_OK; - } - int APS5_VABI setLoopCount(int loopcount) { - std::lock_guard lock(gFmodMutex); - loopCount = loopcount; - return FMOD_OK; - } - int APS5_VABI getCurrentSound(Sound** sound) { - std::lock_guard lock(gFmodMutex); - if (sound) *sound = this->sound; - return FMOD_OK; - } -}; - -class ChannelGroup : public ChannelControl { -public: - std::string name; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI addGroup(ChannelGroup* group, bool propagatedspclock, DSPConnection** connection) { - (void)group; (void)propagatedspclock; - std::lock_guard lock(gFmodMutex); - if (connection) *connection = nullptr; - return FMOD_OK; - } - int APS5_VABI getNumGroups(int* numgroups) { - std::lock_guard lock(gFmodMutex); - if (numgroups) *numgroups = 0; - return FMOD_OK; - } - int APS5_VABI getGroup(int index, ChannelGroup** group) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI getParentGroup(ChannelGroup** group) { - std::lock_guard lock(gFmodMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI getName(char* name, int namelen) { - std::lock_guard lock(gFmodMutex); - if (name && namelen > 0) { - std::strncpy(name, this->name.c_str(), static_cast(namelen - 1)); - name[namelen - 1] = '\0'; - } - return FMOD_OK; - } - int APS5_VABI getNumChannels(int* numchannels) { - std::lock_guard lock(gFmodMutex); - if (numchannels) *numchannels = 0; - return FMOD_OK; - } - int APS5_VABI getChannel(int index, Channel** channel) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (channel) *channel = nullptr; - return FMOD_OK; - } -}; - -class Geometry { -public: - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI addPolygon(float directocclusion, float reverbocclusion, bool doublesided, int numvertices, const FMOD_VECTOR* vertices, int* polygonindex) { - (void)directocclusion; (void)reverbocclusion; (void)doublesided; (void)numvertices; (void)vertices; - std::lock_guard lock(gFmodMutex); - if (polygonindex) *polygonindex = 0; - return FMOD_OK; - } - int APS5_VABI setActive(bool active) { - (void)active; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class Reverb3D { -public: - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI set3DAttributes(const FMOD_VECTOR* pos, float mindistance, float maxdistance) { - (void)pos; (void)mindistance; (void)maxdistance; - return FMOD_OK; - } - int APS5_VABI setActive(bool active) { - (void)active; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -static std::vector gFmodChannels; - -static int APS5_VABI SoundReleaseImpl(Sound* self) { - std::lock_guard lock(gFmodMutex); - for (Channel* ch : gFmodChannels) { - if (ch && ch->sound == self) { - ch->playing = false; - ch->sound = nullptr; - ch->mixHasData = false; - } - } - delete self; - return FMOD_OK; -} - -int APS5_VABI Sound::release() { return SoundReleaseImpl(this); } - -class System { -public: - bool initialized = false; - int maxChannels = 0; - unsigned initFlags = 0; - int driver = 0; - FMOD_OUTPUTTYPE output = FMOD_OUTPUTTYPE_AUTODETECT; - int softwareChannels = 0; - int numListeners = 1; - float dopplerScale = 1.0f; - float distanceFactor = 1.0f; - float rolloffScale = 1.0f; - void* userData = nullptr; - ChannelGroup* masterChannelGroup = nullptr; - SoundGroup* masterSoundGroup = nullptr; - static int APS5_VABI create(System** system) { - std::lock_guard lock(gFmodMutex); - if (!system) return FMOD_OK; - *system = new System(); - return FMOD_OK; - } - static int APS5_VABI create(System** system, unsigned headerversion) { - (void)headerversion; - return create(system); - } - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - // Drop channels owned by this system (game holds raw pointers; mark - // them stopped and free to avoid unbounded growth). - for (auto it = gFmodChannels.begin(); it != gFmodChannels.end();) { - if (*it && (*it)->owner == this) { - delete *it; - it = gFmodChannels.erase(it); - } else { - ++it; - } - } - delete masterChannelGroup; - delete masterSoundGroup; - masterChannelGroup = nullptr; - masterSoundGroup = nullptr; - delete this; - return FMOD_OK; - } - int APS5_VABI init(int maxchannels, unsigned flags, void* extradriverdata) { - (void)extradriverdata; - std::lock_guard lock(gFmodMutex); - maxChannels = maxchannels; - initFlags = flags; - initialized = true; - if (!masterChannelGroup) masterChannelGroup = new ChannelGroup(); - if (!masterSoundGroup) masterSoundGroup = new SoundGroup(); - // Real-audio: try SDL unless NOSOUND output or env requests silent. - // Keep silent backend as fallback (no failure returned). - bool wantSilent = FmodSilentRequested() || - output == FMOD_OUTPUTTYPE_NOSOUND || - output == FMOD_OUTPUTTYPE_NOSOUND_NRT; - if (!wantSilent) { - // Unlock during SDL open (SDL may call back)? FmodEnsureDevice uses - // no FMOD lock internally except SDL, so we can call with lock held - // for simplicity; SDL_OpenAudioDevice does not call into FMOD. - if (!FmodEnsureDevice()) { - APS5_LOG_ERR("%s", "FMOD: SDL audio unavailable, silent fallback"); - } else { - APS5_LOG_OUT("%s", "FMOD: SDL audio output active (48k stereo)"); - } - } - return FMOD_OK; - } - int APS5_VABI close() { - std::lock_guard lock(gFmodMutex); - initialized = false; - return FMOD_OK; - } - int APS5_VABI update() { - std::lock_guard lock(gFmodMutex); - if (!initialized) return FMOD_OK; - if (gFmodSilent || gFmodDevice == 0) return FMOD_OK; // silent fallback - // Throttle like libSceAudioOut: avoid unbounded queue growth. - Uint32 queued = SDL_GetQueuedAudioSize(gFmodDevice); - const Uint32 oneBuf = static_cast(kFmodMixFrames * kFmodOutCh * sizeof(float)); - if (queued > oneBuf * 8u) return FMOD_OK; - float mix[kFmodMixFrames * 2] = {}; - bool anyActive = false; - for (Channel* ch : gFmodChannels) { - if (!ch || ch->owner != this) continue; - if (!ch->playing || ch->paused || ch->mute) continue; - if (!ch->mixHasData || !ch->sound || ch->sound->pcm.empty()) continue; - Sound* s = ch->sound; - const size_t total = ch->mixTotal; - if (total == 0) continue; - size_t cur = ch->mixCursor; - const float vol = ch->volume; - if (vol <= 0.0f) { - // Still advance cursor so silent channels finish. - size_t adv = kFmodMixFrames; - if (!ch->mixLoop) { - if (cur + adv >= total) { cur = total; ch->playing = false; } - else cur += adv; - } else { - cur = (cur + adv) % (total ? total : 1); - } - ch->mixCursor = cur; - ch->position = static_cast(cur); - continue; - } - anyActive = true; - const float* pcm = s->pcm.data(); - size_t pos = cur; - for (int i = 0; i < kFmodMixFrames; i++) { - if (pos >= total) { - if (ch->mixLoop || s->loopCount != 0 || - (s->mode & FMOD_LOOP_NORMAL) != 0) { - pos = 0; - } else { - break; - } - } - mix[i * 2] += pcm[pos * 2] * vol; - mix[i * 2 + 1] += pcm[pos * 2 + 1] * vol; - pos++; - } - // Advance cursor by frames consumed (approx, ignoring loop wrap details above). - size_t adv = static_cast(kFmodMixFrames); - if (!ch->mixLoop && s->loopCount == 0 && (s->mode & FMOD_LOOP_NORMAL) == 0) { - if (cur + adv >= total) { - cur = total; - ch->playing = false; - } else { - cur += adv; - } - } else { - cur = total ? ((cur + adv) % total) : 0; - } - ch->mixCursor = cur; - ch->position = static_cast(cur); - } - if (!anyActive) { - // Still queue silence rarely to keep device alive? No — skip to - // avoid filling queue with silence when nothing plays. - return FMOD_OK; - } - for (int i = 0; i < kFmodMixFrames * 2; i++) { - if (mix[i] > 1.0f) mix[i] = 1.0f; - else if (mix[i] < -1.0f) mix[i] = -1.0f; - } - // Convert to device format if needed. - if (gFmodSpec.format == AUDIO_F32SYS || gFmodSpec.format == 0) { - if (SDL_QueueAudio(gFmodDevice, mix, sizeof(mix)) < 0) { - APS5_LOG_ERR("%s", "FMOD: SDL_QueueAudio failed"); - } - } else { - int16_t tmp[kFmodMixFrames * 2]; - for (int i = 0; i < kFmodMixFrames * 2; i++) { - float v = mix[i] * 32767.0f; - if (v > 32767.0f) v = 32767.0f; - if (v < -32768.0f) v = -32768.0f; - tmp[i] = static_cast(v); - } - // Resample/format convert via SDL if spec differs (channels/rate). - // For minimal path, queue directly when spec is S16 stereo 48k; - // otherwise use SDL_ConvertAudio. - if (gFmodSpec.channels == 2 && gFmodSpec.freq == kFmodOutRate && - (gFmodSpec.format == AUDIO_S16SYS)) { - SDL_QueueAudio(gFmodDevice, tmp, sizeof(tmp)); - } else { - SDL_AudioCVT cvt{}; - if (SDL_BuildAudioCVT(&cvt, AUDIO_F32SYS, 2, kFmodOutRate, - gFmodSpec.format, gFmodSpec.channels, gFmodSpec.freq) >= 0) { - std::vector buf(sizeof(mix) * 4u); - std::memcpy(buf.data(), mix, sizeof(mix)); - cvt.buf = buf.data(); - cvt.len = static_cast(sizeof(mix)); - if (SDL_ConvertAudio(&cvt) == 0) { - SDL_QueueAudio(gFmodDevice, cvt.buf, static_cast(cvt.len_cvt)); - } - } - } - } - return FMOD_OK; - } - int APS5_VABI setOutput(FMOD_OUTPUTTYPE output) { - std::lock_guard lock(gFmodMutex); - this->output = output; - if (output == FMOD_OUTPUTTYPE_NOSOUND || output == FMOD_OUTPUTTYPE_NOSOUND_NRT) { - gFmodSilent = true; - } - return FMOD_OK; - } - int APS5_VABI getOutput(FMOD_OUTPUTTYPE* output) { - std::lock_guard lock(gFmodMutex); - if (output) *output = this->output; - return FMOD_OK; - } - int APS5_VABI getNumDrivers(int* numdrivers) { - std::lock_guard lock(gFmodMutex); - if (numdrivers) *numdrivers = 1; - return FMOD_OK; - } - int APS5_VABI getDriverInfo(int id, char* name, int namelen, FMOD_GUID* guid, int* systemrate, FMOD_SPEAKERMODE* speakermode, int* speakermodechannels) { - (void)id; - std::lock_guard lock(gFmodMutex); - if (name && namelen > 0) { - const char* driverName = (gFmodDevice != 0 && !gFmodSilent) ? "AnyPS5 FMOD Output" : "AnyPS5 Null Output"; - std::strncpy(name, driverName, static_cast(namelen - 1)); - name[namelen - 1] = '\0'; - } - if (guid) std::memset(guid, 0, sizeof(*guid)); - if (systemrate) *systemrate = 48000; - if (speakermode) *speakermode = FMOD_SPEAKERMODE_STEREO; - if (speakermodechannels) *speakermodechannels = 2; - return FMOD_OK; - } - int APS5_VABI setDriver(int driver) { - std::lock_guard lock(gFmodMutex); - this->driver = driver; - return FMOD_OK; - } - int APS5_VABI getDriver(int* driver) { - std::lock_guard lock(gFmodMutex); - if (driver) *driver = this->driver; - return FMOD_OK; - } - int APS5_VABI setSoftwareChannels(int numsoftwarechannels) { - std::lock_guard lock(gFmodMutex); - softwareChannels = numsoftwarechannels; - return FMOD_OK; - } - int APS5_VABI getSoftwareChannels(int* numsoftwarechannels) { - std::lock_guard lock(gFmodMutex); - if (numsoftwarechannels) *numsoftwarechannels = softwareChannels; - return FMOD_OK; - } - int APS5_VABI setSoftwareFormat(int samplerate, FMOD_SPEAKERMODE speakermode, int numrawspeakers) { - (void)samplerate; (void)speakermode; (void)numrawspeakers; - return FMOD_OK; - } - int APS5_VABI getSoftwareFormat(int* samplerate, FMOD_SPEAKERMODE* speakermode, int* numrawspeakers) { - std::lock_guard lock(gFmodMutex); - if (samplerate) *samplerate = 48000; - if (speakermode) *speakermode = FMOD_SPEAKERMODE_STEREO; - if (numrawspeakers) *numrawspeakers = 0; - return FMOD_OK; - } - int APS5_VABI setDSPBufferSize(unsigned bufferlength, int numbuffers) { - (void)bufferlength; (void)numbuffers; - return FMOD_OK; - } - int APS5_VABI getDSPBufferSize(unsigned* bufferlength, int* numbuffers) { - std::lock_guard lock(gFmodMutex); - if (bufferlength) *bufferlength = 1024; - if (numbuffers) *numbuffers = 4; - return FMOD_OK; - } - int APS5_VABI setAdvancedSettings(FMOD_ADVANCEDSETTINGS* settings) { - (void)settings; - return FMOD_OK; - } - int APS5_VABI getAdvancedSettings(FMOD_ADVANCEDSETTINGS* settings) { - std::lock_guard lock(gFmodMutex); - if (settings) std::memset(settings, 0, sizeof(*settings)); - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_SYSTEM_CALLBACK callback, FMOD_SYSTEM_CALLBACK_TYPE callbackmask) { - (void)callback; (void)callbackmask; - return FMOD_OK; - } - int APS5_VABI setFileSystem(void* useropen, void* userclose, void* userread, void* userseek, void* userasyncread, void* userasynccancel, int blockalign) { - (void)useropen; (void)userclose; (void)userread; (void)userseek; - (void)userasyncread; (void)userasynccancel; (void)blockalign; - return FMOD_OK; - } - int APS5_VABI setPluginPath(const char* pluginpath) { - (void)pluginpath; - return FMOD_OK; - } - int APS5_VABI loadPlugin(const char* filename, unsigned* handle, FMOD_PLUGINTYPE plugintype) { - (void)filename; (void)plugintype; - std::lock_guard lock(gFmodMutex); - if (handle) *handle = 0; - return FMOD_OK; - } - int APS5_VABI getVersion(unsigned* version) { - std::lock_guard lock(gFmodMutex); - if (version) *version = FMOD_VERSION_CURRENT; - return FMOD_OK; - } - int APS5_VABI getOutputHandle(void** handle) { - std::lock_guard lock(gFmodMutex); - if (handle) *handle = nullptr; - return FMOD_OK; - } - int APS5_VABI getChannelsPlaying(int* channels, int* realchannels) { - std::lock_guard lock(gFmodMutex); - int active = 0; - for (Channel* ch : gFmodChannels) { - if (!ch || ch->owner != this) continue; - if (!ch->playing || ch->paused) continue; - if (ch->mixHasData) { - if (ch->mixLoop || ch->mixCursor < ch->mixTotal) active++; - } - // Null/empty sounds (smoke test) are not counted to preserve old - // behaviour (previously always returned 0). - } - if (channels) *channels = active; - if (realchannels) *realchannels = active; - return FMOD_OK; - } - int APS5_VABI getCPUUsage(FMOD_CPU_USAGE* usage) { - std::lock_guard lock(gFmodMutex); - if (usage) std::memset(usage, 0, sizeof(*usage)); - return FMOD_OK; - } - int APS5_VABI getFileUsage(long long* samplebytesread, long long* streambytesread, long long* otherbytesread) { - std::lock_guard lock(gFmodMutex); - if (samplebytesread) *samplebytesread = 0; - if (streambytesread) *streambytesread = 0; - if (otherbytesread) *otherbytesread = 0; - return FMOD_OK; - } - int APS5_VABI getSpectrum(float* spectrumarray, int numvalues, int channeloffset, FMOD_DSP_FFT_WINDOW window) { - (void)channeloffset; (void)window; - std::lock_guard lock(gFmodMutex); - if (spectrumarray && numvalues > 0) std::memset(spectrumarray, 0, sizeof(float) * static_cast(numvalues)); - return FMOD_OK; - } - int APS5_VABI getWaveData(float* wavearray, int numvalues, int channeloffset) { - (void)channeloffset; - std::lock_guard lock(gFmodMutex); - if (wavearray && numvalues > 0) std::memset(wavearray, 0, sizeof(float) * static_cast(numvalues)); - return FMOD_OK; - } - int APS5_VABI createSound(const char* name_or_data, unsigned mode, FMOD_CREATESOUNDEXINFO* exinfo, Sound** sound) { - std::lock_guard lock(gFmodMutex); - if (!sound) return FMOD_OK; - Sound* created = new Sound(); - created->mode = mode; - unsigned exLen = exinfo ? exinfo->length : 0; - if (exLen) created->length = exLen; - const bool isMem = (mode & (FMOD_OPENMEMORY_FLAG | FMOD_OPENMEMORY_POINT_FLAG)) != 0; - const bool isRaw = (mode & FMOD_OPENRAW_FLAG) != 0; - if (isMem && name_or_data && exLen > 0 && exLen < (1u << 30)) { - const uint8_t* buf = reinterpret_cast(name_or_data); - // Keep a copy of the name only for file-based sounds; for memory - // buffers the pointer is binary data (not a C string). - created->name.clear(); - std::vector pcm; - int rate = kFmodOutRate, ch = 2; - bool decoded = false; - if (!isRaw) { - decoded = FmodParseWav(buf, exLen, pcm, rate, ch); - } - if (!decoded && isRaw) { - // OPENRAW: interpret buffer using exinfo (default stereo 48k PCM16). - int rawCh = exinfo ? exinfo->numchannels : 0; - int rawRate = exinfo ? exinfo->defaultfrequency : 0; - int rawFmt = exinfo ? static_cast(exinfo->format) : 2; - if (rawCh <= 0 || rawCh > 8) rawCh = 2; - if (rawRate <= 0) rawRate = kFmodOutRate; - size_t bytesPerSamp = 2; - if (rawFmt == 1) bytesPerSamp = 1; // PCM8 - else if (rawFmt == 2) bytesPerSamp = 2; // PCM16 - else if (rawFmt == 5) bytesPerSamp = 4; // FLOAT - else bytesPerSamp = 2; - size_t frames = exLen / (static_cast(rawCh) * bytesPerSamp); - if (frames > 0 && frames < 48000u * 60u) { - pcm.reserve(frames * 2); - for (size_t f = 0; f < frames; f++) { - float l = 0, r = 0; - for (int c = 0; c < rawCh; c++) { - const uint8_t* p = buf + (f * rawCh + c) * bytesPerSamp; - float s = 0; - if (rawFmt == 1) s = (static_cast(p[0]) - 128) / 128.0f; - else if (rawFmt == 2) { int16_t v; std::memcpy(&v, p, 2); s = v / 32768.0f; } - else if (rawFmt == 5) { float v; std::memcpy(&v, p, 4); s = v; } - if (c == 0) l = s; else if (c == 1) r = s; else { l += s; r += s; } - } - if (rawCh == 1) r = l; - pcm.push_back(l); pcm.push_back(r); - } - // Resample if needed. - if (rawRate != kFmodOutRate && !pcm.empty()) { - size_t inF = pcm.size() / 2; - size_t outF = static_cast((static_cast(inF) * kFmodOutRate) / rawRate); - std::vector rs; rs.reserve(outF * 2); - for (size_t i = 0; i < outF; i++) { - double pos = (static_cast(i) * rawRate) / kFmodOutRate; - size_t i0 = static_cast(pos); - size_t i1 = i0 + 1 < inF ? i0 + 1 : i0; - float t = static_cast(pos - i0); - rs.push_back(pcm[i0 * 2] * (1 - t) + pcm[i1 * 2] * t); - rs.push_back(pcm[i0 * 2 + 1] * (1 - t) + pcm[i1 * 2 + 1] * t); - } - pcm.swap(rs); - } - decoded = !pcm.empty(); - rate = kFmodOutRate; ch = 2; - } - } - if (decoded) { - created->pcm.swap(pcm); - created->pcmRate = rate; - created->pcmCh = ch; - } else { - // Compressed (Vorbis/MP3/FSB/AT9...) or unknown: audible - // placeholder so the mixer path can be heard. Real decoders - // (Vorbis/MP3/AT9) remain future work — see docs/dev/TechnicalDebt. - FmodMakeSine(created->pcm, kFmodOutRate, 2, 440.0f, 1.0f); - created->pcmRate = kFmodOutRate; - created->pcmCh = 2; - } - } else { - if (name_or_data) { - // File-based: store name only (first bytes may still be checked - // for embedded RIFF in case game passes a path that is data). - size_t nlen = std::strlen(name_or_data); - if (nlen < 256) created->name = name_or_data; - } - // Placeholder 0.5s sine so file-based playSound is audible. - FmodMakeSine(created->pcm, kFmodOutRate, 2, 330.0f, 0.5f); - created->pcmRate = kFmodOutRate; - created->pcmCh = 2; - } - *sound = created; - return FMOD_OK; - } - int APS5_VABI createStream(const char* name_or_data, unsigned mode, FMOD_CREATESOUNDEXINFO* exinfo, Sound** sound) { - return createSound(name_or_data, mode, exinfo, sound); - } - int APS5_VABI createDSP(FMOD_DSP_DESCRIPTION* description, DSP** dsp) { - (void)description; - std::lock_guard lock(gFmodMutex); - if (dsp) *dsp = new DSP(); - return FMOD_OK; - } - int APS5_VABI createDSPByType(FMOD_DSP_TYPE type, DSP** dsp) { - std::lock_guard lock(gFmodMutex); - if (dsp) { - *dsp = new DSP(); - (*dsp)->type = type; - } - return FMOD_OK; - } - int APS5_VABI playSound(Sound* sound, ChannelGroup* channelgroup, bool paused, Channel** channel) { - std::lock_guard lock(gFmodMutex); - if (!channel) return FMOD_OK; - Channel* created = new Channel(); - created->sound = sound; - created->group = channelgroup ? channelgroup : masterChannelGroup; - created->paused = paused; - created->playing = true; - created->owner = this; - created->mixCursor = 0; - if (sound && !sound->pcm.empty()) { - created->mixHasData = true; - created->mixTotal = sound->pcm.size() / 2; - const bool loopMode = (sound->mode & FMOD_LOOP_NORMAL) != 0; - created->mixLoop = loopMode || sound->loopCount != 0; - } else { - created->mixHasData = false; - created->mixTotal = 0; - created->mixLoop = false; - } - gFmodChannels.push_back(created); - *channel = created; - return FMOD_OK; - } - int APS5_VABI playDSP(DSP* dsp, ChannelGroup* channelgroup, bool paused, Channel** channel) { - (void)dsp; - std::lock_guard lock(gFmodMutex); - if (!channel) return FMOD_OK; - Channel* created = new Channel(); - created->group = channelgroup ? channelgroup : masterChannelGroup; - created->paused = paused; - created->playing = true; - created->owner = this; - created->mixHasData = false; - gFmodChannels.push_back(created); - *channel = created; - return FMOD_OK; - } - int APS5_VABI getChannel(int channelid, Channel** channel) { - (void)channelid; - std::lock_guard lock(gFmodMutex); - if (channel) *channel = nullptr; - return FMOD_OK; - } - int APS5_VABI getMasterSoundGroup(SoundGroup** soundgroup) { - std::lock_guard lock(gFmodMutex); - if (soundgroup) *soundgroup = masterSoundGroup; - return FMOD_OK; - } - int APS5_VABI getMasterChannelGroup(ChannelGroup** channelgroup) { - std::lock_guard lock(gFmodMutex); - if (channelgroup) *channelgroup = masterChannelGroup; - return FMOD_OK; - } - int APS5_VABI setReverbProperties(int instance, FMOD_REVERB_PROPERTIES* prop) { - (void)instance; (void)prop; - return FMOD_OK; - } - int APS5_VABI getReverbProperties(int instance, FMOD_REVERB_PROPERTIES* prop) { - (void)instance; - std::lock_guard lock(gFmodMutex); - if (prop) std::memset(prop, 0, sizeof(*prop)); - return FMOD_OK; - } - int APS5_VABI createReverb3D(Reverb3D** reverb) { - std::lock_guard lock(gFmodMutex); - if (reverb) *reverb = new Reverb3D(); - return FMOD_OK; - } - int APS5_VABI createGeometry(int maxpolygons, int maxvertices, Geometry** geometry) { - (void)maxpolygons; (void)maxvertices; - std::lock_guard lock(gFmodMutex); - if (geometry) *geometry = new Geometry(); - return FMOD_OK; - } - int APS5_VABI loadGeometry(const void* data, int datasize, Geometry** geometry) { - (void)data; (void)datasize; - std::lock_guard lock(gFmodMutex); - if (geometry) *geometry = new Geometry(); - return FMOD_OK; - } - int APS5_VABI setGeometrySettings(float maxworldsize) { - (void)maxworldsize; - return FMOD_OK; - } - int APS5_VABI getGeometrySettings(float* maxworldsize) { - std::lock_guard lock(gFmodMutex); - if (maxworldsize) *maxworldsize = 0.0f; - return FMOD_OK; - } - int APS5_VABI set3DNumListeners(int numlisteners) { - std::lock_guard lock(gFmodMutex); - numListeners = numlisteners; - return FMOD_OK; - } - int APS5_VABI get3DNumListeners(int* numlisteners) { - std::lock_guard lock(gFmodMutex); - if (numlisteners) *numlisteners = numListeners; - return FMOD_OK; - } - int APS5_VABI set3DListenerAttributes(int listener, const FMOD_VECTOR* pos, const FMOD_VECTOR* vel, const FMOD_VECTOR* forward, const FMOD_VECTOR* up) { - (void)listener; (void)pos; (void)vel; (void)forward; (void)up; - return FMOD_OK; - } - int APS5_VABI mixerSuspend() { return FMOD_OK; } - int APS5_VABI mixerResume() { return FMOD_OK; } - int APS5_VABI set3DSettings(float dopplerscale, float distancefactor, float rolloffscale) { - std::lock_guard lock(gFmodMutex); - dopplerScale = dopplerscale; - distanceFactor = distancefactor; - rolloffScale = rolloffscale; - return FMOD_OK; - } - int APS5_VABI get3DSettings(float* dopplerscale, float* distancefactor, float* rolloffscale) { - std::lock_guard lock(gFmodMutex); - if (dopplerscale) *dopplerscale = dopplerScale; - if (distancefactor) *distancefactor = distanceFactor; - if (rolloffscale) *rolloffscale = rolloffScale; - return FMOD_OK; - } - int APS5_VABI createChannelGroup(const char* name, ChannelGroup** channelgroup) { - std::lock_guard lock(gFmodMutex); - if (!channelgroup) return FMOD_OK; - *channelgroup = new ChannelGroup(); - if (name) (*channelgroup)->name = name; - return FMOD_OK; - } - int APS5_VABI createSoundGroup(SoundGroup** soundgroup) { - std::lock_guard lock(gFmodMutex); - if (soundgroup) *soundgroup = new SoundGroup(); - return FMOD_OK; - } - int APS5_VABI lockDSP() { return FMOD_OK; } - int APS5_VABI unlockDSP() { return FMOD_OK; } - int APS5_VABI getSoundRAM(int* currentalloced, int* maxalloced, int* total) { - std::lock_guard lock(gFmodMutex); - if (currentalloced) *currentalloced = 0; - if (maxalloced) *maxalloced = 0; - if (total) *total = 0; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI setStreamBufferSize(unsigned filebuffersize, unsigned filebuffersizetype) { - (void)filebuffersize; (void)filebuffersizetype; - return FMOD_OK; - } - int APS5_VABI attachChannelGroupToPort(FMOD_PORT_TYPE porttype, unsigned long long portindex, ChannelGroup* channelgroup) { - (void)porttype; (void)portindex; (void)channelgroup; - return FMOD_OK; - } - int APS5_VABI detachChannelGroupFromPort(ChannelGroup* channelgroup) { - (void)channelgroup; - return FMOD_OK; - } - int APS5_VABI setNetworkProxy(const char* proxy) { - (void)proxy; - return FMOD_OK; - } -}; - -int APS5_VABI Sound::getSystemObject(System** system) { - std::lock_guard lock(gFmodMutex); - if (system) *system = nullptr; - return FMOD_OK; -} - -int APS5_VABI ChannelControl::getSystemObject(System** system) { - std::lock_guard lock(gFmodMutex); - if (system) *system = nullptr; - return FMOD_OK; -} - -} - -extern "C" { - -int APS5_VABI FMOD_System_Create(FMOD_SYSTEM** system) { - std::lock_guard lock(gFmodMutex); - if (!system) return 0; - *system = reinterpret_cast(new FMOD::System()); - return 0; -} - -int APS5_VABI FMOD_Memory_Initialize(void* poolmem, int poollen, void* useralloc, void* userrealloc, void* userfree, unsigned memtypeflags) { - (void)poolmem; (void)poollen; (void)useralloc; (void)userrealloc; (void)userfree; (void)memtypeflags; - return 0; -} - -int APS5_VABI FMOD_Memory_GetStats(int* currentalloced, int* maxalloced, int blocking) { - (void)blocking; - std::lock_guard lock(gFmodMutex); - if (currentalloced) *currentalloced = 0; - if (maxalloced) *maxalloced = 0; - return 0; -} - -namespace { - -// Round2 NID identification via real FMOD headers (const-correct manglings). -// VS1Vg5yOLH0 = FMOD::System::setFileSystem(open,close,read,seek,asyncRead,asyncCancel,blockalign) -// WAU72lnwYic = FMOD::System::setCallback(callback,mask) -// Both are ABI-compatible with void*/int (func ptrs are 8B, mask 4B); we keep -// the silent backend (return OK) — file callbacks are not needed for -// OPENMEMORY sounds used by Legends, and callbacks are stored nowhere. -__attribute__((used)) int APS5_VABI fmod_system_setfilesystem(void* self, void* a1, void* a2, void* a3, void* a4, void* a5, void* a6, int a7) { - (void)self; (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; (void)a6; (void)a7; - return 0; -} -__attribute__((used)) int APS5_VABI fmod_system_setcallback(void* self, void* cb, unsigned mask) { - (void)self; (void)cb; (void)mask; - return 0; -} - -} - -APS5_EXPORT("VS1Vg5yOLH0", fmod_system_setfilesystem); -APS5_EXPORT("WAU72lnwYic", fmod_system_setcallback); - -} diff --git a/core/libs/prx/libfmodstudio/CMakeLists.txt b/core/libs/prx/libfmodstudio/CMakeLists.txt deleted file mode 100644 index 38c8ce484..000000000 --- a/core/libs/prx/libfmodstudio/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -add_library(libfmodstudio SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp -) - -target_include_directories(libfmodstudio PRIVATE - ${LIBS_INCLUDE_DIR} -) -target_link_libraries(libfmodstudio PRIVATE libkernel libc) -target_compile_options(libfmodstudio PRIVATE -fkeep-inline-functions) -configure_windows_unwind(libfmodstudio) diff --git a/core/libs/prx/libfmodstudio/Export.cpp b/core/libs/prx/libfmodstudio/Export.cpp deleted file mode 100644 index 8f7baefc3..000000000 --- a/core/libs/prx/libfmodstudio/Export.cpp +++ /dev/null @@ -1,1007 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -enum FMOD_STUDIO_STOP_MODE { - FMOD_STUDIO_STOP_ALLOWFADEOUT = 0, - FMOD_STUDIO_STOP_IMMEDIATE = 1, - FMOD_STUDIO_STOP_MAX = 2 -}; - -enum FMOD_STUDIO_PLAYBACK_STATE { - FMOD_STUDIO_PLAYBACK_PLAYING = 0, - FMOD_STUDIO_PLAYBACK_SUSTAINING = 1, - FMOD_STUDIO_PLAYBACK_STOPPED = 2, - FMOD_STUDIO_PLAYBACK_STARTING = 3, - FMOD_STUDIO_PLAYBACK_STOPPING = 4, - FMOD_STUDIO_PLAYBACK_MAX = 5 -}; - -enum FMOD_STUDIO_LOADING_STATE { - FMOD_STUDIO_LOADING_STATE_UNLOADING = 0, - FMOD_STUDIO_LOADING_STATE_UNLOADED = 1, - FMOD_STUDIO_LOADING_STATE_LOADING = 2, - FMOD_STUDIO_LOADING_STATE_LOADED = 3, - FMOD_STUDIO_LOADING_STATE_ERROR = 4, - FMOD_STUDIO_LOADING_STATE_MAX = 5 -}; - -enum FMOD_STUDIO_EVENT_PROPERTY { - FMOD_STUDIO_EVENT_PROPERTY_CHANNELPRIORITY = 0, - FMOD_STUDIO_EVENT_PROPERTY_SCHEDULE_DELAY = 1, - FMOD_STUDIO_EVENT_PROPERTY_SCHEDULE_LOOKAHEAD = 2, - FMOD_STUDIO_EVENT_PROPERTY_MINIMUM_DISTANCE = 3, - FMOD_STUDIO_EVENT_PROPERTY_MAXIMUM_DISTANCE = 4, - FMOD_STUDIO_EVENT_PROPERTY_MAX = 5 -}; - -enum FMOD_STUDIO_SYSTEM_CALLBACK_TYPE { - FMOD_STUDIO_SYSTEM_CALLBACK_PREUPDATE = 1, - FMOD_STUDIO_SYSTEM_CALLBACK_POSTUPDATE = 2, - FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD = 4, - FMOD_STUDIO_SYSTEM_CALLBACK_ALL = 0xFFFFFFFF -}; - -enum FMOD_STUDIO_EVENT_CALLBACK_TYPE { - FMOD_STUDIO_EVENT_CALLBACK_CREATED = 1, - FMOD_STUDIO_EVENT_CALLBACK_DESTROYED = 2, - FMOD_STUDIO_EVENT_CALLBACK_STARTING = 4, - FMOD_STUDIO_EVENT_CALLBACK_STARTED = 8, - FMOD_STUDIO_EVENT_CALLBACK_RESTARTED = 16, - FMOD_STUDIO_EVENT_CALLBACK_STOPPED = 32, - FMOD_STUDIO_EVENT_CALLBACK_START_FAILED = 64, - FMOD_STUDIO_EVENT_CALLBACK_ALL = 0xFFFFFFFF -}; - -struct FMOD_GUID { - unsigned Data1; - unsigned short Data2; - unsigned short Data3; - unsigned char Data4[8]; -}; - -struct FMOD_VECTOR { - float x; - float y; - float z; -}; - -struct FMOD_3D_ATTRIBUTES { - FMOD_VECTOR position; - FMOD_VECTOR velocity; - FMOD_VECTOR forward; - FMOD_VECTOR up; -}; - -struct FMOD_CHANNELGROUP { - int unused; -}; - -struct FMOD_DSP_DESCRIPTION { - unsigned pluginsdkversion; - char name[32]; - unsigned version; -}; - -struct FMOD_STUDIO_PARAMETER_ID { - unsigned data1; - unsigned data2; -}; - -struct FMOD_STUDIO_PARAMETER_DESCRIPTION { - char name[256]; - float minimum; - float maximum; - float defaultvalue; -}; - -struct FMOD_STUDIO_BANK_INFO { - int size; - void* userdata; - int userdatalength; -}; - -struct FMOD_STUDIO_ADVANCEDSETTINGS { - int cbsize; - unsigned commandqueuesize; - unsigned handleinitialsize; -}; - -struct FMOD_STUDIO_CPU_USAGE { - float update; - float nonstreaming; - float streaming; -}; - -struct FMOD_STUDIO_BUFFER_USAGE { - int studiocommandqueue; - int studiohandle; - int values[6]; -}; - -struct FMOD_STUDIO_MEMORY_USAGE { - int exclusive; - int inclusive; - int sampledata; -}; - -struct FMOD_STUDIO_SOUND_INFO { - const char* name_or_data; - unsigned mode; - int subsoundindex; -}; - -struct FMOD_STUDIO_SYSTEM { - int unused; -}; - -struct FMOD_STUDIO_EVENTINSTANCE { - int unused; -}; - -typedef int (*FMOD_STUDIO_SYSTEM_CALLBACK)(FMOD_STUDIO_SYSTEM* system, FMOD_STUDIO_SYSTEM_CALLBACK_TYPE type, void* commanddata, void* userdata); -typedef int (*FMOD_STUDIO_EVENT_CALLBACK)(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE* event, void* parameters); - -namespace { - -std::mutex gStudioMutex; - -constexpr int FMOD_OK = 0; - -} - -namespace FMOD { -namespace Studio { - -class System; -class Bank; -class EventDescription; -class EventInstance; -class Bus; -class VCA; -class CommandReplay; - -class ID : public FMOD_GUID { -}; - -class Bank { -public: - std::string path; - bool sampleDataLoaded = false; - void* userData = nullptr; - bool APS5_VABI isValid() { - return true; - } - int APS5_VABI getID(ID* id) { - std::lock_guard lock(gStudioMutex); - if (id) std::memset(id, 0, sizeof(*id)); - return FMOD_OK; - } - int APS5_VABI getPath(char* path, int size, int* retrieved) { - std::lock_guard lock(gStudioMutex); - if (path && size > 0) { - std::strncpy(path, this->path.c_str(), static_cast(size - 1)); - path[size - 1] = '\0'; - } - if (retrieved) *retrieved = static_cast(this->path.size() + 1); - return FMOD_OK; - } - int APS5_VABI unload() { - std::lock_guard lock(gStudioMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI loadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = true; - return FMOD_OK; - } - int APS5_VABI unloadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = false; - return FMOD_OK; - } - int APS5_VABI getLoadingState(FMOD_STUDIO_LOADING_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = FMOD_STUDIO_LOADING_STATE_LOADED; - return FMOD_OK; - } - int APS5_VABI getSampleLoadingState(FMOD_STUDIO_LOADING_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = sampleDataLoaded ? FMOD_STUDIO_LOADING_STATE_LOADED : FMOD_STUDIO_LOADING_STATE_UNLOADED; - return FMOD_OK; - } - int APS5_VABI getStringCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getEventCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getEventList(EventDescription** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getBusCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getBusList(Bus** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getVCACount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getVCAList(VCA** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class EventDescription { -public: - std::string path; - bool sampleDataLoaded = false; - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getID(ID* id) { - std::lock_guard lock(gStudioMutex); - if (id) std::memset(id, 0, sizeof(*id)); - return FMOD_OK; - } - int APS5_VABI getPath(char* path, int size, int* retrieved) { - std::lock_guard lock(gStudioMutex); - if (path && size > 0) { - std::strncpy(path, this->path.c_str(), static_cast(size - 1)); - path[size - 1] = '\0'; - } - if (retrieved) *retrieved = static_cast(this->path.size() + 1); - return FMOD_OK; - } - int APS5_VABI getInstanceCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getInstanceList(EventInstance** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI createInstance(EventInstance** instance); - int APS5_VABI loadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = true; - return FMOD_OK; - } - int APS5_VABI unloadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = false; - return FMOD_OK; - } - int APS5_VABI getSampleLoadingState(FMOD_STUDIO_LOADING_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = sampleDataLoaded ? FMOD_STUDIO_LOADING_STATE_LOADED : FMOD_STUDIO_LOADING_STATE_UNLOADED; - return FMOD_OK; - } - int APS5_VABI getLength(int* length) { - std::lock_guard lock(gStudioMutex); - if (length) *length = 0; - return FMOD_OK; - } - int APS5_VABI isSnapshot(bool* snapshot) { - std::lock_guard lock(gStudioMutex); - if (snapshot) *snapshot = false; - return FMOD_OK; - } - int APS5_VABI isOneshot(bool* oneshot) { - std::lock_guard lock(gStudioMutex); - if (oneshot) *oneshot = false; - return FMOD_OK; - } - int APS5_VABI isStream(bool* isstream) { - std::lock_guard lock(gStudioMutex); - if (isstream) *isstream = false; - return FMOD_OK; - } - int APS5_VABI is3D(bool* is3d) { - std::lock_guard lock(gStudioMutex); - if (is3d) *is3d = false; - return FMOD_OK; - } - int APS5_VABI hasSustainPoint(bool* sustainpoint) { - std::lock_guard lock(gStudioMutex); - if (sustainpoint) *sustainpoint = false; - return FMOD_OK; - } - int APS5_VABI getMinimumDistance(float* distance) { - std::lock_guard lock(gStudioMutex); - if (distance) *distance = 0.0f; - return FMOD_OK; - } - int APS5_VABI getMaximumDistance(float* distance) { - std::lock_guard lock(gStudioMutex); - if (distance) *distance = 10000.0f; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class EventInstance { -public: - EventDescription* description = nullptr; - bool playing = false; - bool paused = false; - float volume = 1.0f; - float pitch = 1.0f; - void* userData = nullptr; - std::unordered_map paramsByName; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getDescription(EventDescription** description) { - std::lock_guard lock(gStudioMutex); - if (description) *description = this->description; - return FMOD_OK; - } - int APS5_VABI getVolume(float* volume, float* finalvolume) { - std::lock_guard lock(gStudioMutex); - if (volume) *volume = this->volume; - if (finalvolume) *finalvolume = this->volume; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gStudioMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getPitch(float* pitch, float* finalpitch) { - std::lock_guard lock(gStudioMutex); - if (pitch) *pitch = this->pitch; - if (finalpitch) *finalpitch = this->pitch; - return FMOD_OK; - } - int APS5_VABI setPitch(float pitch) { - std::lock_guard lock(gStudioMutex); - this->pitch = pitch; - return FMOD_OK; - } - int APS5_VABI getPaused(bool* paused) { - std::lock_guard lock(gStudioMutex); - if (paused) *paused = this->paused; - return FMOD_OK; - } - int APS5_VABI setPaused(bool paused) { - std::lock_guard lock(gStudioMutex); - this->paused = paused; - return FMOD_OK; - } - int APS5_VABI start() { - std::lock_guard lock(gStudioMutex); - playing = true; - return FMOD_OK; - } - int APS5_VABI stop(FMOD_STUDIO_STOP_MODE mode) { - (void)mode; - std::lock_guard lock(gStudioMutex); - playing = false; - return FMOD_OK; - } - int APS5_VABI release() { - std::lock_guard lock(gStudioMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI getPlaybackState(FMOD_STUDIO_PLAYBACK_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = playing ? FMOD_STUDIO_PLAYBACK_PLAYING : FMOD_STUDIO_PLAYBACK_STOPPED; - return FMOD_OK; - } - int APS5_VABI setTimelinePosition(int position) { - (void)position; - return FMOD_OK; - } - int APS5_VABI getTimelinePosition(int* position) { - std::lock_guard lock(gStudioMutex); - if (position) *position = 0; - return FMOD_OK; - } - int APS5_VABI setParameterByName(const char* name, float value, bool ignoreseekspeed) { - (void)ignoreseekspeed; - std::lock_guard lock(gStudioMutex); - if (name) paramsByName[name] = value; - return FMOD_OK; - } - int APS5_VABI getParameterByName(const char* name, float* value, float* finalvalue) { - std::lock_guard lock(gStudioMutex); - float stored = 0.0f; - if (name) { - auto it = paramsByName.find(name); - if (it != paramsByName.end()) stored = it->second; - } - if (value) *value = stored; - if (finalvalue) *finalvalue = stored; - return FMOD_OK; - } - int APS5_VABI setParameterByID(FMOD_STUDIO_PARAMETER_ID id, float value, bool ignoreseekspeed) { - (void)id; (void)ignoreseekspeed; - return FMOD_OK; - } - int APS5_VABI keyOff() { return FMOD_OK; } - int APS5_VABI setReverbLevel(int index, float level) { - (void)index; (void)level; - return FMOD_OK; - } - int APS5_VABI set3DAttributes(const FMOD_3D_ATTRIBUTES* attributes) { - (void)attributes; - return FMOD_OK; - } - int APS5_VABI setListenerMask(unsigned mask) { - (void)mask; - return FMOD_OK; - } - int APS5_VABI getChannelGroup(FMOD_CHANNELGROUP** group) { - std::lock_guard lock(gStudioMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_STUDIO_EVENT_CALLBACK callback) { - (void)callback; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI hasSustainPoint(bool* sustainpoint) { - std::lock_guard lock(gStudioMutex); - if (sustainpoint) *sustainpoint = false; - return FMOD_OK; - } - int APS5_VABI triggerCue() { return FMOD_OK; } -}; - -int APS5_VABI EventDescription::createInstance(EventInstance** instance) { - std::lock_guard lock(gStudioMutex); - if (!instance) return FMOD_OK; - *instance = new EventInstance(); - (*instance)->description = this; - return FMOD_OK; -} - -class Bus { -public: - std::string path; - float volume = 1.0f; - bool paused = false; - bool mute = false; - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getVolume(float* volume, float* finalvolume) { - std::lock_guard lock(gStudioMutex); - if (volume) *volume = this->volume; - if (finalvolume) *finalvolume = this->volume; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gStudioMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getPaused(bool* paused) { - std::lock_guard lock(gStudioMutex); - if (paused) *paused = this->paused; - return FMOD_OK; - } - int APS5_VABI setPaused(bool paused) { - std::lock_guard lock(gStudioMutex); - this->paused = paused; - return FMOD_OK; - } - int APS5_VABI getMute(bool* mute) { - std::lock_guard lock(gStudioMutex); - if (mute) *mute = this->mute; - return FMOD_OK; - } - int APS5_VABI setMute(bool mute) { - std::lock_guard lock(gStudioMutex); - this->mute = mute; - return FMOD_OK; - } - int APS5_VABI stopAllEvents(FMOD_STUDIO_STOP_MODE mode) { - (void)mode; - return FMOD_OK; - } - int APS5_VABI getChannelGroup(FMOD_CHANNELGROUP** group) { - std::lock_guard lock(gStudioMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI lockChannelGroup() { return FMOD_OK; } - int APS5_VABI unlockChannelGroup() { return FMOD_OK; } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class VCA { -public: - std::string path; - float volume = 1.0f; - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getVolume(float* volume, float* finalvolume) { - std::lock_guard lock(gStudioMutex); - if (volume) *volume = this->volume; - if (finalvolume) *finalvolume = this->volume; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gStudioMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class CommandReplay { -public: - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getLength(float* length) { - std::lock_guard lock(gStudioMutex); - if (length) *length = 0.0f; - return FMOD_OK; - } - int APS5_VABI start() { return FMOD_OK; } - int APS5_VABI stop() { return FMOD_OK; } - int APS5_VABI seekToTime(float time) { - (void)time; - return FMOD_OK; - } - int APS5_VABI release() { - std::lock_guard lock(gStudioMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class System { -public: - bool initialized = false; - int maxChannels = 0; - int numListeners = 1; - void* userData = nullptr; - std::unordered_map paramsByName; - std::vector banks; - static int APS5_VABI create(System** system, unsigned headerversion) { - (void)headerversion; - std::lock_guard lock(gStudioMutex); - if (!system) return FMOD_OK; - *system = new System(); - return FMOD_OK; - } - int APS5_VABI isValid() { - return true; - } - int APS5_VABI release() { - std::lock_guard lock(gStudioMutex); - for (Bank* bank : banks) delete bank; - banks.clear(); - delete this; - return FMOD_OK; - } - int APS5_VABI initialize(int maxchannels, unsigned studioflags, unsigned flags, void* extradriverdata) { - (void)studioflags; (void)flags; (void)extradriverdata; - std::lock_guard lock(gStudioMutex); - maxChannels = maxchannels; - initialized = true; - return FMOD_OK; - } - int APS5_VABI update() { return FMOD_OK; } - int APS5_VABI getEvent(const char* pathOrID, EventDescription** event) { - std::lock_guard lock(gStudioMutex); - if (!event) return FMOD_OK; - *event = new EventDescription(); - if (pathOrID) (*event)->path = pathOrID; - return FMOD_OK; - } - int APS5_VABI getBankList(Bank** array, int capacity, int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = static_cast(banks.size()); - if (array && capacity > 0) { - int filled = capacity < static_cast(banks.size()) ? capacity : static_cast(banks.size()); - for (int i = 0; i < filled; i++) array[i] = banks[static_cast(i)]; - } - return FMOD_OK; - } - int APS5_VABI getBankCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = static_cast(banks.size()); - return FMOD_OK; - } - int APS5_VABI loadBankFile(const char* filename, unsigned flags, Bank** bank) { - (void)flags; - std::lock_guard lock(gStudioMutex); - if (!bank) return FMOD_OK; - *bank = new Bank(); - if (filename) (*bank)->path = filename; - banks.push_back(*bank); - return FMOD_OK; - } - int APS5_VABI loadBankMemory(const char* buffer, int length, unsigned mode, unsigned flags, Bank** bank) { - (void)buffer; (void)length; (void)mode; (void)flags; - std::lock_guard lock(gStudioMutex); - if (!bank) return FMOD_OK; - *bank = new Bank(); - banks.push_back(*bank); - return FMOD_OK; - } - int APS5_VABI unloadAll() { - std::lock_guard lock(gStudioMutex); - for (Bank* bank : banks) delete bank; - banks.clear(); - return FMOD_OK; - } - int APS5_VABI flushCommands() { return FMOD_OK; } - int APS5_VABI flushSampleLoading() { return FMOD_OK; } - int APS5_VABI setNumListeners(int numlisteners) { - std::lock_guard lock(gStudioMutex); - numListeners = numlisteners; - return FMOD_OK; - } - int APS5_VABI getNumListeners(int* numlisteners) { - std::lock_guard lock(gStudioMutex); - if (numlisteners) *numlisteners = numListeners; - return FMOD_OK; - } - int APS5_VABI setListenerAttributes(int index, const FMOD_3D_ATTRIBUTES* attributes, const FMOD_VECTOR* attenuationposition) { - (void)index; (void)attributes; (void)attenuationposition; - return FMOD_OK; - } - int APS5_VABI getBus(const char* pathOrID, Bus** bus) { - std::lock_guard lock(gStudioMutex); - if (!bus) return FMOD_OK; - *bus = new Bus(); - if (pathOrID) (*bus)->path = pathOrID; - return FMOD_OK; - } - int APS5_VABI getVCA(const char* pathOrID, VCA** vca) { - std::lock_guard lock(gStudioMutex); - if (!vca) return FMOD_OK; - *vca = new VCA(); - if (pathOrID) (*vca)->path = pathOrID; - return FMOD_OK; - } - int APS5_VABI getParameterByName(const char* name, float* value, float* finalvalue) { - std::lock_guard lock(gStudioMutex); - float stored = 0.0f; - if (name) { - auto it = paramsByName.find(name); - if (it != paramsByName.end()) stored = it->second; - } - if (value) *value = stored; - if (finalvalue) *finalvalue = stored; - return FMOD_OK; - } - int APS5_VABI setParameterByName(const char* name, float value, bool ignoreseekspeed) { - (void)ignoreseekspeed; - std::lock_guard lock(gStudioMutex); - if (name) paramsByName[name] = value; - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_STUDIO_SYSTEM_CALLBACK callback, FMOD_STUDIO_SYSTEM_CALLBACK_TYPE callbackmask) { - (void)callback; (void)callbackmask; - return FMOD_OK; - } - int APS5_VABI registerPlugin(const FMOD_DSP_DESCRIPTION* description) { - (void)description; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI getCPUUsage(FMOD_STUDIO_CPU_USAGE* usage) { - std::lock_guard lock(gStudioMutex); - if (usage) std::memset(usage, 0, sizeof(*usage)); - return FMOD_OK; - } - int APS5_VABI getBufferUsage(FMOD_STUDIO_BUFFER_USAGE* usage) { - std::lock_guard lock(gStudioMutex); - if (usage) std::memset(usage, 0, sizeof(*usage)); - return FMOD_OK; - } - int APS5_VABI resetBufferUsage() { return FMOD_OK; } - int APS5_VABI getMemoryUsage(FMOD_STUDIO_MEMORY_USAGE* memoryusage) { - std::lock_guard lock(gStudioMutex); - if (memoryusage) std::memset(memoryusage, 0, sizeof(*memoryusage)); - return FMOD_OK; - } -}; - -} -} - -extern "C" { - -namespace { - -// Round2: 20/25 studio stubs identified via real FMOD headers (const-correct -// Itanium manglings, verified with g++ + c++filt + NidCompute SHA1+suffix). -// Wrappers below are ABI-compatible (this+params, SysV) with the real const -// methods; most delegate to the existing non-const logic (const is -// compile-time only, same calling convention). - -// +V2QcIcvWpw = EventDescription::createInstance(EventInstance**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_createInstance(const void* self, void** out) { - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - auto* desc = const_cast(static_cast(self)); - auto* inst = new FMOD::Studio::EventInstance(); - inst->description = desc; - *out = inst; - return 0; -} -// 2QOSvz5hvAg = EventInstance::getPlaybackState(PLAYBACK_STATE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getPlaybackState(const void* self, int* state) { - std::lock_guard lock(gStudioMutex); - const auto* inst = static_cast(self); - if (state) *state = (inst && inst->playing) ? 0 : 2; // PLAYING=0, STOPPED=2 - return 0; -} -// 3loRRCoqXfU = EventInstance::isValid() const -__attribute__((used)) int APS5_VABI fmodstudio_real_ei_isValid(const void* self) { - (void)self; - return 1; -} -// 6E6MUTGjxhI = System::getEvent(const char*, EventDescription**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getEvent(const void* self, const char* path, void** out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - auto* ed = new FMOD::Studio::EventDescription(); - if (path) ed->path = path; - *out = ed; - return 0; -} -// 6nWYF1yMOl4 = Bank::getLoadingState(LOADING_STATE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_bankLoading(const void* self, int* state) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (state) *state = 3; // LOADED - return 0; -} -// 7hd4bRJuLMg = System::getCoreSystem(CoreSystem**) const (placeholder 256B zeroed) -__attribute__((used)) int APS5_VABI fmodstudio_real_getCoreSystem(const void* self, void** out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - void* placeholder = std::calloc(1, 256); - *out = placeholder; - return 0; -} -// -7VafNNns2A = EventInstance::getUserData(void**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_ei_getUserData(const void* self, void** out) { - std::lock_guard lock(gStudioMutex); - const auto* inst = static_cast(self); - if (out) *out = inst ? inst->userData : nullptr; - return 0; -} -// AdR01fo-WaM = System::getParameterByName(const char*, float*, float*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_sysGetParam(const void* self, const char* name, float* v, float* fv) { - std::lock_guard lock(gStudioMutex); - const auto* sys = static_cast(self); - float stored = 0.0f; - if (sys && name) { - auto it = sys->paramsByName.find(name); - if (it != sys->paramsByName.end()) stored = it->second; - } - if (v) *v = stored; - if (fv) *fv = stored; - return 0; -} -// alleMuKEWr8 = EventDescription::isOneshot(bool*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_isOneshot(const void* self, bool* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = false; - return 0; -} -// Bu8uFP7ME2k = System::getVCA(const char*, VCA**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getVCA(const void* self, const char* path, void** out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - auto* vca = new FMOD::Studio::VCA(); - if (path) vca->path = path; - *out = vca; - return 0; -} -// DsFsPD0MWNc = System::getSoundInfo(const char*, SOUND_INFO*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getSoundInfo(const void* self, const char* key, void* info) { - (void)self; (void)key; - std::lock_guard lock(gStudioMutex); - if (info) std::memset(info, 0, sizeof(FMOD_STUDIO_SOUND_INFO)); - return 0; -} -// --f9RJwAh1A = EventDescription::getSampleLoadingState(LOADING_STATE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_edSampleState(const void* self, int* state) { - std::lock_guard lock(gStudioMutex); - const auto* ed = static_cast(self); - if (state) *state = (ed && ed->sampleDataLoaded) ? 3 : 1; // LOADED=3, UNLOADED=1 - return 0; -} -// fbAVXdyBuso = Bank::isValid() const -__attribute__((used)) int APS5_VABI fmodstudio_real_bankIsValid(const void* self) { - (void)self; - return 1; -} -// LG53EJZJDnA = EventInstance::setCallback(callback, mask) (non-const, 2 params) -__attribute__((used)) int APS5_VABI fmodstudio_real_eiSetCallback(void* self, void* cb, unsigned mask) { - (void)self; (void)cb; (void)mask; - return 0; -} -// nhBPjhZ+VWs = EventDescription::is3D(bool*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_is3D(const void* self, bool* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = false; - return 0; -} -// omDBr+dcDVc = EventDescription::getInstanceCount(int*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getInstanceCount(const void* self, int* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = 0; - return 0; -} -// uWXqw4LeM5Q = EventDescription::isValid() const -__attribute__((used)) int APS5_VABI fmodstudio_real_edIsValid(const void* self) { - (void)self; - return 1; -} -// VdQjbIsdhXQ = EventDescription::getLength(int*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_edGetLength(const void* self, int* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = 0; - return 0; -} -// WP51b8simn8 = EventInstance::getTimelinePosition(int*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getTimeline(const void* self, int* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = 0; - return 0; -} -// Zi1n4MvZ3-0 = System::getBufferUsage(BUFFER_USAGE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getBufferUsage(const void* self, void* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) std::memset(out, 0, sizeof(FMOD_STUDIO_BUFFER_USAGE)); - return 0; -} - -__attribute__((used)) int fmodstudio_nid_stub_10() { NotImplemented_nid_no_patch("Cm2cmtCv8cA"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_12() { NotImplemented_nid_no_patch("EfM9xXvBmxk"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_15() { NotImplemented_nid_no_patch("Js90KQXVS4s"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_18() { NotImplemented_nid_no_patch("oCYWES02VPc"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_22() { NotImplemented_nid_no_patch("vVwA2cZA5e4"); return 0; } - -} - -APS5_EXPORT("+V2QcIcvWpw", fmodstudio_real_createInstance); -APS5_EXPORT("2QOSvz5hvAg", fmodstudio_real_getPlaybackState); -APS5_EXPORT("3loRRCoqXfU", fmodstudio_real_ei_isValid); -APS5_EXPORT("6E6MUTGjxhI", fmodstudio_real_getEvent); -APS5_EXPORT("6nWYF1yMOl4", fmodstudio_real_bankLoading); -APS5_EXPORT("7hd4bRJuLMg", fmodstudio_real_getCoreSystem); -APS5_EXPORT("-7VafNNns2A", fmodstudio_real_ei_getUserData); -APS5_EXPORT("AdR01fo-WaM", fmodstudio_real_sysGetParam); -APS5_EXPORT("alleMuKEWr8", fmodstudio_real_isOneshot); -APS5_EXPORT("Bu8uFP7ME2k", fmodstudio_real_getVCA); -APS5_EXPORT("Cm2cmtCv8cA", fmodstudio_nid_stub_10); -APS5_EXPORT("DsFsPD0MWNc", fmodstudio_real_getSoundInfo); -APS5_EXPORT("EfM9xXvBmxk", fmodstudio_nid_stub_12); -APS5_EXPORT("--f9RJwAh1A", fmodstudio_real_edSampleState); -APS5_EXPORT("fbAVXdyBuso", fmodstudio_real_bankIsValid); -APS5_EXPORT("Js90KQXVS4s", fmodstudio_nid_stub_15); -APS5_EXPORT("LG53EJZJDnA", fmodstudio_real_eiSetCallback); -APS5_EXPORT("nhBPjhZ+VWs", fmodstudio_real_is3D); -APS5_EXPORT("oCYWES02VPc", fmodstudio_nid_stub_18); -APS5_EXPORT("omDBr+dcDVc", fmodstudio_real_getInstanceCount); -APS5_EXPORT("uWXqw4LeM5Q", fmodstudio_real_edIsValid); -APS5_EXPORT("VdQjbIsdhXQ", fmodstudio_real_edGetLength); -APS5_EXPORT("vVwA2cZA5e4", fmodstudio_nid_stub_22); -APS5_EXPORT("WP51b8simn8", fmodstudio_real_getTimeline); -APS5_EXPORT("Zi1n4MvZ3-0", fmodstudio_real_getBufferUsage); - -} diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp deleted file mode 100644 index 8b5c7b585..000000000 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ /dev/null @@ -1,256 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include - -using Deleter = void (APS5_VABI*)(void*); - -struct IntrusivePtr { - void* object; - Deleter deleter; - void* libContext; -}; - -using Construct = void (APS5_VABI*)(IntrusivePtr*); -using Copy = void (APS5_VABI*)(IntrusivePtr*, const IntrusivePtr*); -using Destroy = void (APS5_VABI*)(IntrusivePtr*); -using Access = void* (APS5_VABI*)(const IntrusivePtr*); - -extern "C" { -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr*); - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr*); - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr*); -} - -static void Require(bool value) { if (!value) std::abort(); } - -static constexpr std::size_t ObjectSize = 0x40; -static constexpr std::size_t VectorReferenceCountOffset = 0x30; - -struct DestroyCase { - Destroy destroy; - std::size_t referenceCountOffset; -}; - -static void* deletedObject = nullptr; -static int deleterCalls = 0; - -static void APS5_VABI RecordDelete(void* object) { - deletedObject = object; - ++deleterCalls; -} - -static std::int32_t ReadCount(const unsigned char* object, const std::size_t offset) { - std::int32_t value; - std::memcpy(&value, object + offset, sizeof(value)); - return value; -} - -static void WriteCount(unsigned char* object, const std::size_t offset, const std::int32_t value) { - std::memcpy(object + offset, &value, sizeof(value)); -} - -static bool OnlyCountChanged(const unsigned char* object, const std::size_t offset) { - for (std::size_t index = 0; index < ObjectSize; ++index) { - if (index >= offset && index < offset + sizeof(std::int32_t)) continue; - if (object[index] != 0x5a) return false; - } - return true; -} - -static void TestConstruct(const Construct construct) { - IntrusivePtr pointer; - std::memset(&pointer, 0xa5, sizeof(pointer)); - construct(&pointer); - Require(pointer.object == nullptr); - Require(pointer.deleter == nullptr); - Require(pointer.libContext == nullptr); -} - -static void TestAccess(const Access access) { - int object = 0; - int context = 0; - const IntrusivePtr empty{nullptr, RecordDelete, &context}; - Require(access(&empty) == nullptr); - const IntrusivePtr filled{&object, nullptr, nullptr}; - Require(access(&filled) == &object); -} - -static void TestCopy(const Copy copy) { - alignas(8) unsigned char object[ObjectSize]; - std::memset(object, 0x5a, sizeof(object)); - WriteCount(object, 0, 1); - int context = 0; - - const IntrusivePtr source{object, RecordDelete, &context}; - IntrusivePtr target; - std::memset(&target, 0xa5, sizeof(target)); - copy(&target, &source); - Require(target.object == object); - Require(target.deleter == RecordDelete); - Require(target.libContext == &context); - Require(source.object == object); - Require(ReadCount(object, 0) == 2); - Require(OnlyCountChanged(object, 0)); - - const IntrusivePtr empty{nullptr, RecordDelete, &context}; - std::memset(&target, 0xa5, sizeof(target)); - copy(&target, &empty); - Require(target.object == nullptr); - Require(target.deleter == RecordDelete); - Require(target.libContext == &context); -} - -static void TestDestroy(const DestroyCase& testCase) { - alignas(8) unsigned char object[ObjectSize]; - std::memset(object, 0x5a, sizeof(object)); - int context = 0; - const std::size_t offset = testCase.referenceCountOffset; - - deleterCalls = 0; - deletedObject = nullptr; - - IntrusivePtr empty{nullptr, RecordDelete, &context}; - testCase.destroy(&empty); - Require(empty.object == nullptr); - Require(empty.deleter == RecordDelete); - Require(empty.libContext == &context); - Require(deleterCalls == 0); - - WriteCount(object, offset, 2); - IntrusivePtr shared{object, RecordDelete, &context}; - testCase.destroy(&shared); - Require(ReadCount(object, offset) == 1); - Require(shared.object == object); - Require(shared.deleter == RecordDelete); - Require(shared.libContext == &context); - Require(deleterCalls == 0); - - IntrusivePtr last{object, RecordDelete, &context}; - testCase.destroy(&last); - Require(ReadCount(object, offset) == 0); - Require(OnlyCountChanged(object, offset)); - Require(deleterCalls == 1); - Require(deletedObject == object); - Require(last.object == nullptr); - Require(last.deleter == nullptr); - Require(last.libContext == &context); - - WriteCount(object, offset, 1); - IntrusivePtr pooled{object, nullptr, &context}; - bool thrown = false; - try { - testCase.destroy(&pooled); - } catch (const std::runtime_error&) { - thrown = true; - } - Require(thrown); - Require(deleterCalls == 1); -} - -static void TestSharedOwnership() { - alignas(8) unsigned char object[ObjectSize]; - std::memset(object, 0x5a, sizeof(object)); - WriteCount(object, 0, 1); - deleterCalls = 0; - deletedObject = nullptr; - - IntrusivePtr owner; - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(&owner); - owner.object = object; - owner.deleter = RecordDelete; - - IntrusivePtr argument; - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(&argument, &owner); - Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&argument) == object); - Require(ReadCount(object, 0) == 2); - - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&argument); - Require(ReadCount(object, 0) == 1); - Require(deleterCalls == 0); - - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&owner); - Require(ReadCount(object, 0) == 0); - Require(deleterCalls == 1); - Require(deletedObject == object); - Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&owner) == nullptr); -} - -int main() { - Require(sizeof(IntrusivePtr) == 0x18); - - const Construct constructors[] = { - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev, - }; - for (const auto construct : constructors) TestConstruct(construct); - - const Access accessors[] = { - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv, - }; - for (const auto access : accessors) TestAccess(access); - - const Copy copies[] = { - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, - }; - for (const auto copy : copies) TestCopy(copy); - - const DestroyCase destructors[] = { - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev, VectorReferenceCountOffset}, - }; - for (const auto& testCase : destructors) TestDestroy(testCase); - - TestSharedOwnership(); - return 0; -} diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp deleted file mode 100644 index c54ee17c3..000000000 --- a/core/libs/tests/GuestNpCppWebApiString.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include - -extern "C" { -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(void* self, void* libContext); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(void* self); -const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const void* self); -} - -namespace { - -void Require(bool value) { if (!value) std::abort(); } - -constexpr std::size_t kStringSize = 0x20; -constexpr std::size_t kGuardSize = 0x10; -constexpr std::size_t kReferenceCountOffset = 0x00; -constexpr std::size_t kBufferOffset = 0x08; -constexpr std::size_t kBufferSizeOffset = 0x10; -constexpr std::size_t kLibContextOffset = 0x18; -constexpr unsigned char kFill = 0xA5; - -struct Storage { - alignas(8) unsigned char bytes[kStringSize + kGuardSize]; -}; - -template -TValue Load(const Storage& storage, std::size_t offset) { - TValue value; - std::memcpy(&value, storage.bytes + offset, sizeof(value)); - return value; -} - -template -void Store(Storage& storage, std::size_t offset, TValue value) { - std::memcpy(storage.bytes + offset, &value, sizeof(value)); -} - -bool GuardIntact(const Storage& storage) { - for (std::size_t i = kStringSize; i < sizeof(storage.bytes); i++) { - if (storage.bytes[i] != kFill) return false; - } - return true; -} - -bool DestructorThrows(Storage& storage) { - try { - _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - -void TestConstructedStringIsEmpty() { - std::uint64_t libContext = 1; - Storage storage; - std::memset(storage.bytes, kFill, sizeof(storage.bytes)); - - _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); - - Require(Load(storage, kReferenceCountOffset) == 0); - Require(Load(storage, kBufferOffset) == nullptr); - Require(Load(storage, kBufferSizeOffset) == 0); - Require(Load(storage, kLibContextOffset) == &libContext); - Require(GuardIntact(storage)); - - const char* text = _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes); - Require(text != nullptr && text[0] == '\0'); - - Storage before = storage; - _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); - Require(std::memcmp(before.bytes, storage.bytes, sizeof(storage.bytes)) == 0); -} - -void TestNullLibContextIsStored() { - Storage storage; - std::memset(storage.bytes, kFill, sizeof(storage.bytes)); - - _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, nullptr); - - Require(Load(storage, kLibContextOffset) == nullptr); - Require(Load(storage, kBufferOffset) == nullptr); - Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes)[0] == '\0'); - _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); -} - -void TestBufferIsReturnedAndNotDroppedSilently() { - std::uint64_t libContext = 1; - char text[] = "ranking"; - Storage storage; - std::memset(storage.bytes, kFill, sizeof(storage.bytes)); - - _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); - Store(storage, kBufferOffset, text); - Store(storage, kBufferSizeOffset, sizeof(text)); - - Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes) == text); - Require(DestructorThrows(storage)); - Require(std::strcmp(text, "ranking") == 0); -} - -} - -int main() { - TestConstructedStringIsEmpty(); - TestNullLibContextIsStored(); - TestBufferIsReturnedAndNotDroppedSilently(); -} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cd0f433c1..657c1707a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -108,10 +108,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceHttpSetRequestStatusCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - no reference implements it (shadPS4 only logs and returns 0); when the callback runs and what status it gets is unknown -- [sce::Np::CppWebApi::Common::IntrusivePtr](../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x18-byte layout (object, deleter, `LibContext*`) and the offset of the 32-bit reference count in the pointee (0x00, 0x30 for `Vector`) come from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws on the last reference of a pointer without a deleter: destroying the object needs its destructor and the `LibContext` allocator, which are not implemented - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sce::Np::CppWebApi::Common::String](../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws for a non-null buffer: releasing it needs the `LibContext` allocator, which is not implemented - [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature diff --git a/tools/package_release.py b/tools/package_release.py index 25296672b..53add7365 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -11,7 +11,6 @@ def package(platform, build, output, version): raise ValueError(f"Invalid release tag for asset filenames: {version}") libraries = sorted((build / "core/libs/libs").glob("*.prx")) expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} - expected.add("libcohtml.Prospero.prx") missing = expected - {library.name for library in libraries} if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") From 4e5a1fe9d44f58550e9302ae417300b286562deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 01:37:05 +0200 Subject: [PATCH 027/203] feat(rtc): parse RFC 2822 and asctime strings in sceRtcParseDateTime Co-Authored-By: Claude --- core/libs/prx/libSceRtc/Export.cpp | 113 ++++++++++++++++++++++++----- core/libs/tests/GuestRtc.cpp | 17 +++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 08bf4bb06..78cedfc66 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -158,6 +159,97 @@ bool parseZoneSuffix(const char*& cursor, std::int64_t& offsetMinutes) { return false; } +bool parseName(const char*& cursor, const char* const* names, int count, int& index) { + for (index = 0; index < count; ++index) { + if (std::strncmp(cursor, names[index], 3) == 0) { + cursor += 3; + return true; + } + } + return false; +} + +bool parseWeekday(const char*& cursor) { + static constexpr const char* names[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + int index = 0; + return parseName(cursor, names, 7, index); +} + +bool parseMonth(const char*& cursor, int& month) { + static constexpr const char* names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + if (!parseName(cursor, names, 12, month)) return false; + ++month; + return true; +} + +bool parseClock(const char*& cursor, int& hour, int& minute, int& second) { + return parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second); +} + +RtcDateTime makeDateTime(int year, int month, int day, int hour, int minute, int second, std::uint32_t microsecond) { + return RtcDateTime{static_cast(year), static_cast(month), static_cast(day), + static_cast(hour), static_cast(minute), static_cast(second), microsecond}; +} + +bool parseIsoDateTime(const char* cursor, RtcDateTime& time, std::int64_t& offsetMinutes) { + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseDigits(cursor, 4, year) || *cursor++ != '-' || !parseDigits(cursor, 2, month) || *cursor++ != '-' || !parseDigits(cursor, 2, day)) return false; + if (*cursor != 'T' && *cursor != 't' && *cursor != ' ') return false; + ++cursor; + if (!parseClock(cursor, hour, minute, second)) return false; + std::uint32_t microsecond = 0; + if (*cursor == '.') { + ++cursor; + std::uint32_t scale = 100000; + if (*cursor < '0' || *cursor > '9') return false; + for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + } + offsetMinutes = 0; + if (*cursor != 0 && !parseZoneSuffix(cursor, offsetMinutes)) return false; + if (*cursor != 0) return false; + time = makeDateTime(year, month, day, hour, minute, second, microsecond); + return true; +} + +bool parseRfc2822DateTime(const char* cursor, RtcDateTime& time, std::int64_t& offsetMinutes) { + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseWeekday(cursor) || *cursor++ != ',' || *cursor++ != ' ' || !parseDigits(cursor, 2, day) || *cursor++ != ' ') return false; + if (!parseMonth(cursor, month) || *cursor++ != ' ' || !parseDigits(cursor, 4, year) || *cursor++ != ' ' || !parseClock(cursor, hour, minute, second)) return false; + offsetMinutes = 0; + if (*cursor == ' ') { + ++cursor; + if (std::strcmp(cursor, "GMT") == 0) { + cursor += 3; + } else if (*cursor == '+' || *cursor == '-') { + const int sign = *cursor++ == '-' ? -1 : 1; + int offsetHours = 0, offsetMinute = 0; + if (!parseDigits(cursor, 2, offsetHours) || !parseDigits(cursor, 2, offsetMinute)) return false; + if (offsetHours > 23 || offsetMinute > 59) return false; + offsetMinutes = sign * (offsetHours * 60 + offsetMinute); + } else { + return false; + } + } + if (*cursor != 0) return false; + time = makeDateTime(year, month, day, hour, minute, second, 0); + return true; +} + +bool parseAsctimeDateTime(const char* cursor, RtcDateTime& time, std::int64_t& offsetMinutes) { + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseWeekday(cursor) || *cursor++ != ' ' || !parseMonth(cursor, month) || *cursor++ != ' ') return false; + if (*cursor == ' ') { + ++cursor; + if (!parseDigits(cursor, 1, day)) return false; + } else if (!parseDigits(cursor, 2, day)) { + return false; + } + if (*cursor++ != ' ' || !parseClock(cursor, hour, minute, second) || *cursor++ != ' ' || !parseDigits(cursor, 4, year) || *cursor != 0) return false; + offsetMinutes = 0; + time = makeDateTime(year, month, day, hour, minute, second, 0); + return true; +} + } extern "C" { @@ -313,28 +405,13 @@ int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; - const char* cursor = date_time; - int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; - bool ok = parseDigits(cursor, 4, year) && *cursor++ == '-' && parseDigits(cursor, 2, month) && *cursor++ == '-' && parseDigits(cursor, 2, day); - ok = ok && (*cursor == 'T' || *cursor == 't' || *cursor == ' '); - if (ok) ++cursor; - ok = ok && parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second); - std::uint32_t microsecond = 0; - if (ok && *cursor == '.') { - ++cursor; - std::uint32_t scale = 100000; - ok = *cursor >= '0' && *cursor <= '9'; - for (; ok && *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; - } + RtcDateTime time{}; std::int64_t offsetMinutes = 0; - if (ok && *cursor != 0) ok = parseZoneSuffix(cursor, offsetMinutes); - if (ok && *cursor != 0) ok = false; - if (!ok) { + if (!parseIsoDateTime(date_time, time, offsetMinutes) && !parseRfc2822DateTime(date_time, time, offsetMinutes) + && !parseAsctimeDateTime(date_time, time, offsetMinutes)) { NotImplemented_nid_no_patch(__func__); return SCE_RTC_ERROR_BAD_PARSE; } - const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), - static_cast(hour), static_cast(minute), static_cast(second), microsecond}; if (const int result = validate(&time); result != 0) return result; const RtcTick local{toTick(time)}; return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 2d843ebfe..a9d053914 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -150,12 +150,29 @@ int main() { Require(sceRtcParseDateTime(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); Require(sceRtcParseDateTime(&tick, "2023-02-29T00:00:00") == invalidDay); Require(sceRtcParseDateTime(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 12:34:56") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 12:34:56 GMT") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 14:04:56 +0130") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 11:04:56 -0130") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 01 Jan 1970 00:00:00 +0000") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseDateTime(&tick, "Thu, 30 Feb 2024 00:00:00") == invalidDay); + Require(sceRtcParseDateTime(&tick, "Thu Feb 29 12:34:56 2024") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu Jan 1 00:00:00 1970") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseDateTime(&tick, "Thu Feb 30 00:00:00 2024") == invalidDay); const char* unparseable[] = { "2024-02-29", "2024/02/29T12:34:56", "2024-02-29T12:34:56Zjunk", "2024-02-29T12:34:56+99:99", "2024-02-29T12:34:56.", + "Thu, 29 Feb 2024 12:34:56 +01:30", + "Thu, 29 Feb 2024 12:34:56 PST", + "Thu, 29 Xyz 2024 12:34:56", + "Xyz, 29 Feb 2024 12:34:56", + "Thu, 29 Feb 2024 12:34:56 +2400", + "Thu Feb 29 12:34:56 2024\n", + "Thu Feb 29 12:34:56 24", + "Thu Feb 29 12:34:56 2024", }; for (const char* text : unparseable) { bool thrown = false; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cd0f433c1..06c6f8654 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -33,7 +33,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown -- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware +- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`), plus the RFC 2822 shape `Ddd, DD Mon YYYY HH:MM:SS[ GMT| ±HHMM]` and the asctime shape `Ddd Mon DD HH:MM:SS YYYY` (day zero- or space-padded) that shadPS4 parses at fixed positions; the weekday name is not checked against the date, a `±HHMM` offset is subtracted to get UTC like the RFC 3339 path (shadPS4 adds it), a string without a zone is taken as UTC, and anything outside these shapes throws instead of returning a parse error, unverified against hardware - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes From 3515ead91a7c28dd51b0f3d79cbbd50bc129b9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:27:27 +0200 Subject: [PATCH 028/203] feat(rtc): implement sceRtcFormatRFC2822 and the RFC 2822/3339 local-time formatters Co-Authored-By: Claude --- core/libs/prx/libSceRtc/Export.cpp | 29 +++++++++++++++ core/libs/tests/GuestRtc.cpp | 60 ++++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 90 insertions(+) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 08bf4bb06..09a4ccbe6 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -107,6 +107,13 @@ std::int64_t localOffsetSeconds(std::uint64_t utcTick) { return localSeconds - static_cast(seconds); } +constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +int localOffsetMinutes(const RtcTick& utc) { + return static_cast(localOffsetSeconds(utc.tick) / 60); +} + int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; if (src->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; @@ -286,6 +293,28 @@ int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_ return 0; } +int APS5_VABI sceRtcFormatRFC3339LocalTime(char* date_time, const RtcTick* utc) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + return sceRtcFormatRFC3339(date_time, utc, localOffsetMinutes(*utc)); +} + +int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_zone_minutes) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; + RtcTick local{}; + if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcDateTime time = fromTick(local.tick); + const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; + std::snprintf(date_time, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, + MONTH_NAMES[time.month - 1], time.year, time.hour, time.minute, time.second, time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + return 0; +} + +int APS5_VABI sceRtcFormatRFC2822LocalTime(char* date_time, const RtcTick* utc) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + return sceRtcFormatRFC2822(date_time, utc, localOffsetMinutes(*utc)); +} + int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; const char* cursor = date_time; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 2d843ebfe..8f8bdcf08 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -30,10 +31,23 @@ int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddMonths(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddYears(RtcTick*, const RtcTick*, std::int16_t); +int APS5_VABI sceRtcFormatRFC3339LocalTime(char*, const RtcTick*); +int APS5_VABI sceRtcFormatRFC2822(char*, const RtcTick*, int); +int APS5_VABI sceRtcFormatRFC2822LocalTime(char*, const RtcTick*); } static void Require(bool value) { if (!value) std::abort(); } +static void SetTimeZone(const char* zone) { +#ifdef _WIN32 + _putenv_s("TZ", zone); + _tzset(); +#else + setenv("TZ", zone, 1); + tzset(); +#endif +} + static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { return left.year == right.year && left.month == right.month && left.day == right.day && left.hour == right.hour && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; @@ -224,4 +238,50 @@ int main() { RtcTick back{}; Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); + + char rfc2822[32]; + tick.tick = leapDayTick; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 90) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 14:04:56 +0130") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -300) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 07:34:56 -0500") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -30) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:04:56 -0030") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 65) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 13:39:56 +0105") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1439) == 0 && std::strcmp(rfc2822, "Fri, 01 Mar 2024 12:33:56 +2359") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1439) == 0 && std::strcmp(rfc2822, "Wed, 28 Feb 2024 12:35:56 -2359") == 0); + const char* week[] = {"Sun, 03 Mar", "Mon, 04 Mar", "Tue, 05 Mar", "Wed, 06 Mar", "Thu, 07 Mar", "Fri, 08 Mar", "Sat, 09 Mar"}; + for (int day = 0; day < 7; ++day) { + const RtcDateTime date{2024, 3, static_cast(3 + day), 0, 0, 0, 0}; + Require(sceRtcGetTick(&date, &tick) == 0 && sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0); + Require(std::strncmp(rfc2822, week[day], std::strlen(week[day])) == 0); + } + tick.tick = 0; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Mon, 01 Jan 0001 00:00:00 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == invalidValue); + tick.tick = maxTick; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Fri, 31 Dec 9999 23:59:59 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = leapDayTick; + for (int offset : {1440, -1440, std::numeric_limits::min(), std::numeric_limits::max()}) { + std::memset(rfc2822, 'x', sizeof(rfc2822)); + char original[sizeof(rfc2822)]; + std::memcpy(original, rfc2822, sizeof(rfc2822)); + Require(sceRtcFormatRFC2822(rfc2822, &tick, offset) == invalidValue); + Require(std::memcmp(rfc2822, original, sizeof(rfc2822)) == 0); + } + Require(sceRtcFormatRFC2822(nullptr, &tick, 0) == invalidPointer); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 0) == invalidPointer); + + SetTimeZone("XXX-5:30"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 18:04:56 +0530") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T18:04:56.78+05:30") == 0); + SetTimeZone("XXX+3"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 09:34:56 -0300") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T09:34:56.78-03:00") == 0); + SetTimeZone("UTC0"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); + Require(sceRtcFormatRFC2822LocalTime(nullptr, &tick) == invalidPointer); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == invalidPointer); + Require(sceRtcFormatRFC3339LocalTime(nullptr, &tick) == invalidPointer); + Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == invalidPointer); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cd0f433c1..cb62288f1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -118,6 +118,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 From ab98cd7f4a37378ac6da46c0863e097fe903472a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:32:51 +0200 Subject: [PATCH 029/203] feat(libkernel): sceKernelReadv, Writev, Preadv and Pwritev Co-Authored-By: Claude --- core/libs/prx/libkernel/CMakeLists.txt | 4 + core/libs/prx/libkernel/File/src/Stdio.cpp | 75 ++++++++++++++ core/libs/tests/GuestFileVector.cpp | 111 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 191 insertions(+) create mode 100644 core/libs/tests/GuestFileVector.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 43949deb4..4af7886fc 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -355,6 +355,10 @@ if(BUILD_TESTING) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) + target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_file_vector_tests PRIVATE libkernel libc) + add_test(NAME guest_file_vector COMMAND guest_file_vector_tests) else() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5785a3c9b..7defbc421 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -17,6 +17,14 @@ #include #include #include + +struct KernelIovec { + void* base; + std::size_t length; +}; + +static constexpr int KERNEL_IOV_MAX = 1024; + #ifdef _WIN32 #include #include @@ -119,6 +127,8 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb #include #include #include +#include +#include #include static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); @@ -156,6 +166,12 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); } +static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); +static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); +static_assert(offsetof(KernelIovec, length) == offsetof(struct iovec, iov_len)); +static const struct iovec* NativeIovecs(const KernelIovec* iov) { + return reinterpret_cast(iov); +} #endif static constexpr int GUEST_ENOENT = 2; @@ -471,6 +487,22 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return pwrite_nid_disambig1_nid_postfix(d, buf, nbytes, offset); } +int64_t APS5_VABI sceKernelReadv(int, const KernelIovec*, int) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelWritev(int, const KernelIovec*, int) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelPreadv(int, const KernelIovec*, int, int64_t) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelPwritev(int, const KernelIovec*, int, int64_t) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + #else int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { @@ -489,6 +521,49 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return result < 0 ? SceErrorFromErrno(errno) : result; } +static int CheckIovecs(const KernelIovec* iov, int iovcnt) { + if (iovcnt < 0 || iovcnt > KERNEL_IOV_MAX) return SceErrorFromErrno(GUEST_EINVAL); + if (iov == nullptr && iovcnt != 0) return SceErrorFromErrno(GUEST_EFAULT); + return 0; +} + +static bool OpenIovecs(const KernelIovec* iov, int iovcnt, std::deque& destinations) { + for (int i = 0; i < iovcnt; ++i) { + if (!destinations.emplace_back(iov[i].base, iov[i].length).Open()) return false; + } + return true; +} + +int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + std::deque destinations; + if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + std::deque destinations; + if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + #endif int APS5_VABI sceKernelRename(const char* from, const char* to) { diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp new file mode 100644 index 000000000..a3cb50f35 --- /dev/null +++ b/core/libs/tests/GuestFileVector.cpp @@ -0,0 +1,111 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { + void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "File vector check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); +static constexpr std::int64_t ErrorEfault = static_cast(0x8002000Eu); +static constexpr std::int64_t ErrorEinval = static_cast(0x80020016u); + +static std::string Contents(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +int main() { + const auto root = std::filesystem::path("anyps5-file-vector-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto path = root / "data.bin"; + { std::ofstream stream(path, std::ios::binary); stream << "0123456789"; } + + const int file = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(file >= 0); + + char first[3] = {}; + char second[4] = {}; + GuestIovec reads[2] = {{first, sizeof(first)}, {second, sizeof(second)}}; + Require(sceKernelReadv(file, reads, 2) == 7); + Require(std::memcmp(first, "012", 3) == 0 && std::memcmp(second, "3456", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 7); + Require(sceKernelReadv(file, reads, 2) == 3); + Require(std::memcmp(first, "789", 3) == 0); + Require(sceKernelReadv(file, reads, 2) == 0); + + Require(sceKernelPreadv(file, reads, 2, 1) == 7); + Require(std::memcmp(first, "123", 3) == 0 && std::memcmp(second, "4567", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 10); + Require(sceKernelPreadv(file, reads, 2, 8) == 2); + Require(std::memcmp(first, "89", 2) == 0); + + char ab[] = "AB"; + char cde[] = "CDE"; + GuestIovec writes[2] = {{ab, 2}, {cde, 3}}; + Require(sceKernelPwritev(file, writes, 2, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 10); + Require(Contents(path) == "01ABCDE789"); + Require(sceKernelLseek(file, 8, 0) == 8); + Require(sceKernelWritev(file, writes, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13); + Require(Contents(path) == "01ABCDE7ABCDE"); + + GuestIovec empty[1] = {{nullptr, 0}}; + Require(sceKernelReadv(file, empty, 1) == 0); + Require(sceKernelReadv(file, reads, 0) == 0); + Require(sceKernelWritev(file, nullptr, 0) == 0); + + Require(sceKernelReadv(file, reads, -1) == ErrorEinval); + Require(sceKernelWritev(file, writes, -1) == ErrorEinval); + Require(sceKernelPreadv(file, reads, -1, 0) == ErrorEinval); + Require(sceKernelPwritev(file, writes, -1, 0) == ErrorEinval); + Require(sceKernelReadv(file, reads, 1025) == ErrorEinval); + Require(sceKernelWritev(file, writes, 1025) == ErrorEinval); + Require(sceKernelReadv(file, nullptr, 1) == ErrorEfault); + Require(sceKernelWritev(file, nullptr, 1) == ErrorEfault); + Require(sceKernelPreadv(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPwritev(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPreadv(file, reads, 2, -1) == ErrorEinval); + Require(sceKernelPwritev(file, writes, 2, -1) == ErrorEinval); + Require(Contents(path) == "01ABCDE7ABCDE"); + + Require(sceKernelClose(file) == 0); + Require(sceKernelReadv(file, reads, 2) == ErrorEbadf); + Require(sceKernelWritev(file, writes, 2) == ErrorEbadf); + Require(sceKernelPreadv(file, reads, 2, 0) == ErrorEbadf); + Require(sceKernelPwritev(file, writes, 2, 0) == ErrorEbadf); + + std::filesystem::remove_all(root); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cd0f433c1..47ca593d1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -120,6 +120,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. +- [sceKernelReadv / sceKernelWritev / sceKernelPreadv / sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) are implemented on Linux only; on Windows they throw - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) From 76ddd9b6d44863f5bbd932905278c80b9c29f82c Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:29:21 +0200 Subject: [PATCH 030/203] feat(agc): implement unnormalized sampler coordinates on single-level 1D and 2D views An S# with FORCE_UNNORMALIZED threw in DecodeSamplerResource. It is now bound through a Vulkan sampler with unnormalizedCoordinates when every use stays where the S# and the host select the same texels. ShaderInfoCollector records the live uses of each sampler after Apply and dead-code elimination, and the emitter shares its explicit-LOD predicate. Populate flags a direct S# with the bit that only explicit-LOD image_sample without offset, comparison, derivatives or _a uses, paired only with direct 1D or 2D images without a format conversion; anything else throws. The driver checks the S# filters, clamps, TRUNC_COORD and MC_COORD_TRUNC, creates the sampler with nearest mips and a zero LOD range, and requires a single-level, single-layer 1D or 2D view of mip 0. The shader disk cache format becomes 5. --- .../Graphics/include/GuestSamplerResource.hpp | 3 +- .../Graphics/include/Sampler.hpp | 11 +- .../Graphics/include/Texture.hpp | 8 + .../Graphics/src/GuestSamplerResource.cpp | 24 +- .../libSceAgcDriver/Graphics/src/Sampler.cpp | 9 +- .../Graphics/src/ShaderResources.cpp | 13 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 + .../tests/GuestSamplerResource.cpp | 74 ++++++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 6 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 156 ++++++++++++ .../execution/ImageSampleUnnormalized.cpp | 240 ++++++++++++++++++ .../IrMetadata/Resources.hpp | 17 ++ .../src/DescriptorBindingBuilder.cpp | 64 +++++ .../Optimization/src/ShaderInfoCollector.cpp | 45 ++++ core/shader/recompiler/Recompiler.hpp | 2 + core/shader/recompiler/ShaderDiskCache.cpp | 10 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 2 +- docs/dev/TechnicalDebt.md | 3 + 19 files changed, 673 insertions(+), 20 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index c64f6726e..f8444bfdb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -25,9 +25,10 @@ struct GuestSamplerResource { VkBorderColor borderColor; bool compareEnable = false; VkCompareOp compareOp = VK_COMPARE_OP_NEVER; + bool unnormalizedCoordinates = false; }; -GuestSamplerResource DecodeSamplerResource(std::span words); +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index e560d391a..67450c4ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -28,17 +28,18 @@ private: VkSampler sampler = VK_NULL_HANDLE; }; -// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use): samplers are -// immutable and DecodeSamplerResource is a pure function of the words, so equal keys mean equal -// samplers and nothing ever invalidates an entry. Holders keep a shared_ptr, so an entry evicted from -// the LRU-capped cache lives on while a descriptor set in flight references it. One cache per device. +// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the +// unnormalized proof): samplers are immutable and DecodeSamplerResource is a pure function of the +// words and the proof, so equal keys mean equal samplers and nothing ever invalidates an entry. +// Holders keep a shared_ptr, so an entry evicted from the LRU-capped cache lives on while a +// descriptor set in flight references it. One cache per device. // APS5_NO_SAMPLER_CACHE=1 creates a sampler per binding as before. class SamplerCache { public: explicit SamplerCache(std::size_t capacity = 1024); SamplerCache(const SamplerCache&) = delete; SamplerCache& operator=(const SamplerCache&) = delete; - std::shared_ptr Get(const Context& context, std::span words, bool compareEnable); + std::shared_ptr Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven = false); // APS5_PROFILE_DRAW counters: lookups served by an existing sampler, and samplers created. std::uint64_t Hits() const { return hits; } std::uint64_t Misses() const { return misses; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 198939b7b..08009e94f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -66,6 +66,12 @@ public: VkImageView View() const; VkImageView FirstLayerView() const { return firstLayerView; } + struct ViewRange { + VkImageViewType type; + std::uint32_t levels; + std::uint32_t layers; + }; + ViewRange SampledViewRange(bool firstLayer) const { return firstLayer ? firstLayerRange : viewRange; } // The layout the image is kept in while sampled. VkImageLayout Layout() const { return layout; } VkDeviceSize AllocationBytes() const { return allocationBytes; } @@ -90,6 +96,8 @@ private: std::shared_ptr owned; VkImageView view = VK_NULL_HANDLE; VkImageView firstLayerView = VK_NULL_HANDLE; + ViewRange viewRange{}; + ViewRange firstLayerRange{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; std::shared_ptr source; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index d28297551..68bcaefa4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -41,7 +41,7 @@ float toSignedLodBias(std::uint32_t raw) { } -GuestSamplerResource DecodeSamplerResource(std::span words) { +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); const auto clampX = (words[0] >> 0u) & 0x7u; @@ -51,6 +51,7 @@ GuestSamplerResource DecodeSamplerResource(std::span words) const auto depthCompareFunc = (words[0] >> 12u) & 0x7u; const auto forceUnormCoords = ((words[0] >> 15u) & 0x1u) != 0; const auto anisoThreshold = (words[0] >> 16u) & 0x7u; + const auto mcCoordTrunc = ((words[0] >> 19u) & 0x1u) != 0; const auto forceSrgb = ((words[0] >> 20u) & 0x1u) != 0; const auto anisoBias = (words[0] >> 21u) & 0x3fu; const auto truncCoord = ((words[0] >> 27u) & 0x1u) != 0; @@ -74,7 +75,17 @@ GuestSamplerResource DecodeSamplerResource(std::span words) const auto borderColorType = (words[3] >> 30u) & 0x3u; - Require(!forceUnormCoords, "guest sampler descriptor uses unnormalized coordinates which are not implemented"); + Require(!forceUnormCoords || unnormalizedProven, "guest sampler descriptor uses unnormalized coordinates which are not implemented"); + if (forceUnormCoords) { + Require(xyMagFilter == xyMinFilter, "guest sampler descriptor uses unnormalized coordinates with different minification and magnification filters, which is not implemented"); + Require(!isAnisoFilter(xyMagFilter), "guest sampler descriptor uses unnormalized coordinates with anisotropic filtering, which is not implemented"); + Require(clampX == 2u || clampX == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampX) + " on X; only clamp-to-last-texel and clamp-to-border are implemented"); + Require(clampY == 2u || clampY == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampY) + " on Y; only clamp-to-last-texel and clamp-to-border are implemented"); + Require(!truncCoord, "guest sampler descriptor uses unnormalized coordinates with TRUNC_COORD, which is not implemented"); + Require(!mcCoordTrunc, "guest sampler descriptor uses unnormalized coordinates with MC_COORD_TRUNC, which is not implemented"); + } else { + Require(!unnormalizedProven, "guest sampler descriptor is bound as unnormalized without FORCE_UNNORMALIZED"); + } Require(!forceSrgb, "guest sampler descriptor forces sRGB decoding which is not implemented"); // TRUNC_COORD picks point-sampled texels by truncation instead of rounding, and the perf fields // trade mip/depth precision for speed; Vulkan's nearest filtering already floors, so these only @@ -131,6 +142,15 @@ GuestSamplerResource DecodeSamplerResource(std::span words) result.borderColor = border; const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS}; result.compareOp = compareOps.at(depthCompareFunc); + if (forceUnormCoords) { + result.unnormalizedCoordinates = true; + result.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + result.minLod = 0.0f; + result.maxLod = 0.0f; + result.lodBias = 0.0f; + result.anisotropyEnable = false; + result.maxAnisotropy = 1.0f; + } return result; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 660b16bb1..b0bed507f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -8,6 +8,7 @@ namespace AgcDriver::Graphics { Require(!descriptor.anisotropyEnable || context.samplerAnisotropy, "guest sampler descriptor requests anisotropic filtering which the device does not support"); Require(descriptor.maxAnisotropy <= context.limits.maxSamplerAnisotropy, "guest sampler descriptor requests an anisotropy ratio beyond the device limit"); Require(descriptor.lodBias >= -context.limits.maxSamplerLodBias && descriptor.lodBias <= context.limits.maxSamplerLodBias, "guest sampler descriptor requests a LOD bias beyond the device limit"); + Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented"); VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; info.magFilter = descriptor.magFilter; @@ -24,7 +25,7 @@ namespace AgcDriver::Graphics { info.minLod = descriptor.minLod; info.maxLod = descriptor.maxLod; info.borderColor = descriptor.borderColor; - info.unnormalizedCoordinates = VK_FALSE; + info.unnormalizedCoordinates = descriptor.unnormalizedCoordinates ? VK_TRUE : VK_FALSE; Check(context.Function("vkCreateSampler")(context.device, &info, nullptr, &sampler), "vkCreateSampler"); } @@ -42,9 +43,9 @@ namespace AgcDriver::Graphics { SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max(capacity, 1)) {} - std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable) { + std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); - const std::array key{words[0], words[1], words[2], words[3], compareEnable ? 1u : 0u}; + const std::array key{words[0], words[1], words[2], words[3], (compareEnable ? 1u : 0u) | (unnormalizedProven ? 2u : 0u)}; std::lock_guard lock(mutex); ++clock; if (const auto found = entries.find(key); found != entries.end()) { @@ -53,7 +54,7 @@ namespace AgcDriver::Graphics { return found->second.sampler; } ++misses; - auto resource = DecodeSamplerResource(words); + auto resource = DecodeSamplerResource(words, unnormalizedProven); resource.compareEnable = compareEnable; auto sampler = std::make_shared(context, resource); // The cap keeps live samplers well below the device's limit (NVIDIA: ~4000); a set in flight diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index c1fff993b..c997899d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1202,6 +1202,8 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha packBits(binding.samplerDepthCompare); packBits(binding.imageDepthCompare); packBits(binding.imageAtomic); + packBits(binding.samplerUnnormalized); + packBits(binding.imageUnnormalized); // Read-only elements are bound without a write set: an object built for one written set // must not serve a build with another (the variant implies it, this makes it explicit). packBits(binding.bufferWritten); @@ -2367,14 +2369,16 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& Require(elementWords == 4, "guest sampler descriptor must contain 4 dwords"); Require(binding.count <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits"); Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); + Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const bool compareEnable = binding.samplerDepthCompare.at(element); + const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; if (context.samplerCache != nullptr && !noSamplerCache) { - samplers.push_back(context.samplerCache->Get(context, words, compareEnable)); + samplers.push_back(context.samplerCache->Get(context, words, compareEnable, unnormalized)); } else { - auto resource = DecodeSamplerResource(words); + auto resource = DecodeSamplerResource(words, unnormalized); resource.compareEnable = compareEnable; samplers.push_back(std::make_shared(context, resource)); } @@ -2533,6 +2537,11 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)); + if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { + const auto range = texture->SampledViewRange(firstLayer); + const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f; + if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast(range.type)) + ")"); + } textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2bb762263..8c223ba09 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -428,6 +428,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex ChainMinLod(context, descriptor, viewInfo, minLod); Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); @@ -472,6 +473,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; ChainMinLod(context, descriptor, viewInfo, minLod); Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView storage view"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); @@ -494,6 +496,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.components = components; viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; } Texture::~Texture() { @@ -505,6 +508,7 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.layerCount = 1; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView first layer"); + firstLayerRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; } void Texture::release() noexcept { diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 4af9a844c..3dd51f261 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -69,6 +69,17 @@ void rejectFields(const Fields& f, std::string_view reason) { reject([&] { DecodeSamplerResource(words); }, reason); } +void rejectUnnormalized(const Fields& f, std::string_view reason) { + const auto words = pack(f); + reject([&] { DecodeSamplerResource(words, true); }, reason); +} + +void requireUnnormalized(const GuestSamplerResource& result, const char* what) { + Require(result.unnormalizedCoordinates, std::string(what) + ": unnormalized coordinates were not decoded"); + Require(result.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST && result.minLod == 0.0f && result.maxLod == 0.0f, std::string(what) + ": unnormalized coordinates need nearest mips and a zero LOD range"); + Require(result.lodBias == 0.0f && !result.anisotropyEnable && result.maxAnisotropy == 1.0f, std::string(what) + ": unnormalized coordinates need no LOD bias and no anisotropy"); +} + bool nearlyEqual(float a, float b) { return std::fabs(a - b) < 0.001f; } @@ -149,6 +160,69 @@ void RunGuestSamplerResourceTests() { badUnorm.forceUnormCoords = true; rejectFields(badUnorm, "unnormalized coordinates"); + const std::array capturedUnnormalized{0x00008092u, 0x00fff000u, 0x05500000u, 0u}; + reject([&] { DecodeSamplerResource(capturedUnnormalized); }, "uses unnormalized coordinates which are not implemented"); + const auto unnormalized = DecodeSamplerResource(capturedUnnormalized, true); + requireUnnormalized(unnormalized, "captured unnormalized S#"); + Require(unnormalized.magFilter == VK_FILTER_LINEAR && unnormalized.minFilter == VK_FILTER_LINEAR, "captured unnormalized S# filters decoded incorrectly"); + Require(unnormalized.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unnormalized.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured unnormalized S# clamp modes decoded incorrectly"); + Require(unnormalized.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "captured unnormalized S# border color decoded incorrectly"); + Require(!DecodeSamplerResource(pack(base)).unnormalizedCoordinates, "a normalized S# decoded to unnormalized coordinates"); + + Fields unnormalizedBase = base; + unnormalizedBase.forceUnormCoords = true; + Fields unnormalizedPoint = unnormalizedBase; + unnormalizedPoint.xyMagFilter = 0; + unnormalizedPoint.xyMinFilter = 0; + const auto point = DecodeSamplerResource(pack(unnormalizedPoint), true); + requireUnnormalized(point, "unnormalized point S#"); + Require(point.magFilter == VK_FILTER_NEAREST && point.minFilter == VK_FILTER_NEAREST, "unnormalized point S# filters decoded incorrectly"); + const std::array borders{VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, VK_BORDER_COLOR_INT_OPAQUE_BLACK, VK_BORDER_COLOR_INT_OPAQUE_WHITE}; + for (std::uint32_t type = 0; type < borders.size(); ++type) { + Fields border = unnormalizedBase; + border.clampX = 6; + border.clampY = 6; + border.borderColorType = type; + const auto decoded = DecodeSamplerResource(pack(border), true); + requireUnnormalized(decoded, "unnormalized clamp-to-border S#"); + Require(decoded.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && decoded.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && decoded.borderColor == borders[type], "unnormalized clamp-to-border S# decoded incorrectly"); + } + for (std::uint32_t mipFilter = 0; mipFilter < 3; ++mipFilter) { + Fields mips = unnormalizedBase; + mips.mipFilter = mipFilter; + requireUnnormalized(DecodeSamplerResource(pack(mips), true), "unnormalized S# mip filter"); + } + Fields unnormalizedBias = unnormalizedBase; + unnormalizedBias.lodBiasRaw = 256; + requireUnnormalized(DecodeSamplerResource(pack(unnormalizedBias), true), "unnormalized S# with a LOD bias"); + Fields unnormalizedMinLod = unnormalizedBase; + unnormalizedMinLod.minLodRaw = 0x100; + unnormalizedMinLod.maxLodRaw = 0x400; + requireUnnormalized(DecodeSamplerResource(pack(unnormalizedMinLod), true), "unnormalized S# with a minimum LOD"); + + Fields unequalFilters = unnormalizedBase; + unequalFilters.xyMinFilter = 0; + rejectUnnormalized(unequalFilters, "unnormalized coordinates with different minification and magnification filters"); + Fields unnormalizedAniso = unnormalizedBase; + unnormalizedAniso.xyMagFilter = 2; + unnormalizedAniso.xyMinFilter = 2; + rejectUnnormalized(unnormalizedAniso, "unnormalized coordinates with anisotropic filtering"); + for (const std::uint32_t clamp : {0u, 1u, 3u, 4u, 5u, 7u}) { + Fields clampX = unnormalizedBase; + clampX.clampX = clamp; + rejectUnnormalized(clampX, "unnormalized coordinates with clamp mode " + std::to_string(clamp) + " on X"); + } + Fields clampY = unnormalizedBase; + clampY.clampY = 0; + rejectUnnormalized(clampY, "unnormalized coordinates with clamp mode 0 on Y"); + Fields unnormalizedTruncated = unnormalizedBase; + unnormalizedTruncated.truncCoord = true; + rejectUnnormalized(unnormalizedTruncated, "unnormalized coordinates with TRUNC_COORD"); + auto truncatedBlend = pack(unnormalizedBase); + truncatedBlend[0] |= 1u << 19u; + reject([&] { DecodeSamplerResource(truncatedBlend, true); }, "unnormalized coordinates with MC_COORD_TRUNC"); + rejectUnnormalized(base, "bound as unnormalized without FORCE_UNNORMALIZED"); + Fields badSrgb = base; badSrgb.forceSrgb = true; rejectFields(badSrgb, "forces sRGB decoding"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 20dd3e202..24da896c3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -31,7 +31,7 @@ void setEnvironment(const char* name, const std::string& value) { } bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { - return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten; + return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -95,6 +95,8 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.imageAtomic = {false, true, false}; binding.bufferAtomic = {true}; binding.bufferWritten = {false, false, true, true, false}; + binding.samplerUnnormalized = {false, true, true}; + binding.imageUnnormalized = {true, false, seed % 2 == 0}; return binding; } @@ -166,7 +168,7 @@ CompiledVariant sampleVariant() { image.indirectSearchIterations = 3; image.indirectResources = {1, 2, 3}; info.info.images = {image}; - info.info.samplers = {{7, 0x10, true, false}}; + info.info.samplers = {{7, 0x10, true, false, SamplerUseExplicitLod | SamplerUseGather}}; info.info.sampledPairs = {{0, 0, 0x10}}; StageInput input{}; input.kind = StageInputKind::GlobalInvocationId; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a65c7d9e5..493303695 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "Optimization/DescriptorBindingBuilder.hpp" #include "Optimization/RequestMemoryView.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" @@ -800,6 +801,159 @@ void verifyComputedTexelOffsets() { } } +void verifyUnnormalizedSamplers() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Format11_11_10UInt = 34; + constexpr std::uint32_t Format32Float = 22; + constexpr std::uint32_t Type2D = 9; + constexpr std::uint32_t Type3D = 10; + constexpr std::uint32_t TypeCube = 11; + constexpr std::uint32_t Type2DArray = 13; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + static std::array output{}; + const auto textureBase = static_cast(reinterpret_cast(texture.bytes.data())); + const auto outputBase = static_cast(reinterpret_cast(output.data())); + const std::array unnormalized{0x00008092u, 0x00fff000u, 0x05500000u, 0u}; + const std::array normalized{0x00000092u, 0x00fff000u, 0x05500000u, 0u}; + const auto imageData = [&](const std::array& sampler, std::uint32_t type, std::uint32_t format, std::uint32_t depth) { + return std::array{ + static_cast(textureBase >> 8u), static_cast((textureBase >> 40u) & 0xffu) | (format << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (type << 28u), depth, 0u, 0u, 0u, + sampler[0], sampler[1], sampler[2], sampler[3], + static_cast(outputBase), static_cast((outputBase >> 32u) & 0xffffu), 64u, 0xfacu}; + }; + const auto userData = [&](const std::array& sampler) { + return imageData(sampler, Type2D, Format8888UNorm, 0u); + }; + const auto program = [](std::uint32_t mimg) { + return std::vector{0x7e020280u, 0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, mimg, 0x00400801u, 0xe0700000u, 0x80030800u, 0xbf810000u}; + }; + const std::array capabilities{1u, static_cast(spv::CapabilityImageGatherExtended), static_cast(spv::CapabilityMinLod)}; + std::uint64_t nextAddress = 0x40000u; + const auto recompile = [&](const std::vector& code, const std::array& data, std::uint64_t address = 0u) { + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, address != 0u ? address : (nextAddress += 0x1000u), code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = data; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + AgcDriver::ShaderMemory memory({}); + static_cast(memory.Capture(request)); + request.context.memory = memory.Regions(); + return Recompile(request); + }; + const auto flags = [](const RecompileResult& result, DescriptorRole role) { + for (const auto& binding : result.bindings) { + if (binding.role == role) return role == DescriptorRole::GuestSamplers ? binding.samplerUnnormalized : binding.imageUnnormalized; + } + throw std::runtime_error("unnormalized samplers: the program has no sampler or image binding"); + }; + const auto proven = [&](const RecompileResult& result, bool expected) { + return flags(result, DescriptorRole::GuestSamplers) == std::vector{expected} && flags(result, DescriptorRole::GuestImages) == std::vector{expected}; + }; + const auto auditSpirv = [](const std::vector& words) { + std::size_t samples = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "unnormalized samplers: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + require(op != spv::OpImageSampleImplicitLod && op != spv::OpImageSampleDrefImplicitLod && op != spv::OpImageSampleDrefExplicitLod && op != spv::OpImageGather && op != spv::OpImageDrefGather && op != spv::OpImageQueryLod, "unnormalized samplers: the SPIR-V samples in a form an unnormalized sampler does not allow"); + if (op == spv::OpImageSampleExplicitLod) { + require(count == 7u && words[cursor + 5] == spv::ImageOperandsLodMask, "unnormalized samplers: an explicit-LOD sample has operands other than Lod"); + ++samples; + } + cursor += count; + } + require(samples != 0u, "unnormalized samplers: the SPIR-V does not sample"); + }; + + const auto lz = program(0xf09c0f08u); + const auto accepted = recompile(lz, userData(unnormalized), 0x40000u); + require(proven(accepted, true), "unnormalized samplers: image_sample_lz through an unnormalized S# was not flagged"); + auditSpirv(accepted.spirv.Words()); + const auto plain = recompile(lz, userData(normalized), 0x40000u); + require(proven(plain, false), "unnormalized samplers: a normalized S# was flagged"); + require(plain.variantId == accepted.variantId && plain.spirv.Words() == accepted.spirv.Words(), "unnormalized samplers: the unnormalized S# compiled another variant"); + + for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u}) { + const auto result = recompile(program(mimg), userData(unnormalized)); + require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b or image_sample_cl was not flagged"); + auditSpirv(result.spirv.Words()); + } + + const auto reject = [&](std::uint32_t mimg, const std::array& data, const char* reason, const char* what) { + expectFailure([&] { static_cast(recompile(program(mimg), data)); }, reason, what); + }; + reject(0xf0c00f08u, userData(unnormalized), "unnormalized guest sampler is used with a texel offset, which is not implemented", "unnormalized samplers: image_sample_o was accepted"); + reject(0xf0bc0f08u, imageData(unnormalized, Type2D, Format32Float, 0u), "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: image_sample_c_lz was accepted"); + reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted"); + reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted"); + reject(0xf0880f08u, userData(unnormalized), "unnormalized guest sampler is used by a sample with derivatives, which is not implemented", "unnormalized samplers: image_sample_d was accepted"); + reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted"); + reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); + reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); + reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted"); + reject(0xf09c0f08u, imageData(unnormalized, Type2D, Format11_11_10UInt, 0u), "unnormalized guest sampler samples an image that needs a format conversion or packed access", "unnormalized samplers: a T# with a format conversion was accepted"); + for (const std::uint32_t mimg : {0xf0c00f08u, 0xf11c0108u, 0xf09c0f18u}) { + static_cast(recompile(program(mimg), imageData(normalized, mimg == 0xf09c0f18u ? TypeCube : Type2D, Format8888UNorm, mimg == 0xf09c0f18u ? 5u : 0u))); + } + + const std::array pixelCode{0xf0800f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + const auto pixelData = userData(unnormalized); + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x4f000u, pixelCode, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.userDataBaseRegister = 0; + pixel.context.userData = pixelData; + pixel.context.pixel = twoParameterPixel(); + pixel.target.vulkanVersion = 0x00401000u; + pixel.target.spirvVersion = 0x00010300u; + pixel.target.subgroupSize = 64; + pixel.layout.pushConstantSizeBytes = 128; + pixel.useCache = false; + expectFailure([&] { static_cast(Recompile(pixel)); }, "unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented", "unnormalized samplers: a pixel image_sample was accepted"); +} + +void verifyUnusedUnnormalizedSampler() { + using namespace ShaderRecompiler; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.numericClass = IrTextureNumericClass::Float; + image.dimension = RdnaImageDimension::Dim2D; + image.read = true; + ShaderInfo info; + info.images = {image}; + info.samplers = {SamplerResource{}}; + info.sampledPairs = {{0u, 0u, 0u}}; + ResourceSnapshot snapshot; + snapshot.images = {DescriptorValue{{0x00001000u, 0x03800000u, 0x0000c000u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}}; + snapshot.samplers = {DescriptorValue{{0x00008092u, 0x00fff000u, 0x05500000u, 0u}, 4u}}; + const auto populate = [&](const ShaderInfo& shader) { + BindingAllocationResult allocation; + allocation.layout.descriptors = {{DescriptorBindingForImage(shader.images[0]), {0u}}, {DescriptorBindingKind::Samplers, {0u}}}; + DescriptorBindingBuilder{}.Populate(allocation, shader, IrShaderStage::Compute, 0u, snapshot, {}); + return allocation.bindings; + }; + const auto bindings = populate(info); + require(bindings.size() == 2u && bindings[0].imageUnnormalized == std::vector{true} && bindings[1].samplerUnnormalized == std::vector{true}, "unnormalized samplers: an S# without live uses was not flagged"); + auto selected = info; + selected.images[0].indirectRoot = 0u; + expectFailure([&] { static_cast(populate(selected)); }, "unnormalized guest sampler samples an image selected at run time, which is not implemented", "unnormalized samplers: an image table root was accepted"); + auto compared = info; + compared.samplers[0].depthCompare = true; + expectFailure([&] { static_cast(populate(compared)); }, "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: a depth-compare S# without live uses was accepted"); + snapshot.samplers[0].dwords[0] = 0x00000092u; + const auto normalized = populate(info); + require(normalized[0].imageUnnormalized == std::vector{false} && normalized[1].samplerUnnormalized == std::vector{false}, "unnormalized samplers: a normalized S# was flagged"); +} + void verifyWaveUniformValues() { using namespace ShaderRecompiler; IrProgram program; @@ -1046,6 +1200,8 @@ int main() { verifyPixelInputs(); verifyPixelParameterSlots(); verifyComputedTexelOffsets(); + verifyUnnormalizedSamplers(); + verifyUnusedUnnormalizedSampler(); verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyFunctionLdsBound(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp new file mode 100644 index 000000000..dc7da4f9a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -0,0 +1,240 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 128; +constexpr std::uint32_t Words = 16; +constexpr std::uint32_t ResultWord = 4; +constexpr std::uint32_t Results = 12; +constexpr std::int32_t Width = 13; +constexpr std::int32_t Height = 7; +constexpr std::uint32_t StorageLevels = 4; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint8_t OtherLevelTexel = 0x40u; +alignas(256) std::array Buffer{}; +alignas(256) std::array SingleLevel{}; +alignas(256) std::array MultiLevel{}; + +alignas(256) constexpr std::array Code{ + 0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0x7e0802ff, 0x402ccccd, + 0xf09c0f08, 0x00610802, 0xf0900f08, 0x00610c02, 0xf0800f08, 0x00611002, 0xbf8c3f70, 0xe0701010, + 0x80000801, 0xe0701014, 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xe0701020, + 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, + 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +constexpr std::array Instructions{"image_sample_lz", "image_sample_l 2.7", "image_sample"}; + +struct SamplerCase { + const char* name; + std::array words; + bool linear; + bool border; +}; + +constexpr std::array Samplers{{ + {"bilinear clamp-to-last-texel, point mips, MAX_LOD 0xfff", {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, true, false}, + {"point clamp-to-last-texel, point mips", {0x00008092u, 0x00fff000u, 0x04000000u, 0u}, false, false}, + {"bilinear clamp-to-last-texel, linear mips, MIN_LOD 1, LOD bias 1.5", {0x00008092u, 0x00fff100u, 0x09500180u, 0u}, true, false}, + {"bilinear clamp-to-border, transparent black", {0x000080b6u, 0x00fff000u, 0x05500000u, 0u}, true, true}, + {"point clamp-to-border, no mips, transparent black", {0x000080b6u, 0u, 0u, 0u}, false, true}, +}}; + +struct Coordinate { + float u; + float v; +}; + +std::vector Coordinates() { + std::vector us; + for (std::int32_t k = 0; k < Width; ++k) { + for (const float fraction : {0.5f, 0.0f, 0.25f, 0.75f}) us.push_back(static_cast(k) + fraction); + } + for (const float edge : {-1.0f, -0.5f, 13.0f, 14.0f, 12.75f}) us.push_back(edge); + constexpr std::array fractions{0.5f, 0.0f, 0.75f}; + std::vector result; + for (std::size_t index = 0; index < us.size(); ++index) { + for (std::size_t fraction = 0; fraction < fractions.size(); ++fraction) { + result.push_back({us[index], static_cast((index * fractions.size() + fraction) % Height) + fractions[fraction]}); + } + } + for (std::int32_t x = 0; x < Width; ++x) { + const float v = static_cast(x % Height) + 0.5f; + result.push_back({static_cast(x) - 1.0f, v}); + result.push_back({static_cast(x) + 2.0f, v}); + } + return result; +} + +std::array TexelOf(std::int32_t x, std::int32_t y) { + return {240.0 * ((x + y) & 1), 16.0 * x + 16.0, 32.0 * y + 32.0, 240.0}; +} + +void FillTexture(std::span texels, std::uint32_t levels) { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Width, Height, levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= texels.size(), "image sample unnormalized: the mip chain does not fit the texel storage"); + std::fill(texels.begin(), texels.end(), std::uint8_t{0xeeu}); + for (std::uint32_t level = 0; level < levels; ++level) { + const auto& mip = mips[level]; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + auto* texel = &texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + const auto value = TexelOf(static_cast(x), static_cast(y)); + for (std::uint32_t component = 0; component < 4u; ++component) { + texel[component] = level == 0u ? static_cast(value[component]) : OtherLevelTexel; + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t lastLevel, std::uint32_t maxMip) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | ((static_cast(Width - 1) & 3u) << 30u), + (static_cast(Width - 1) >> 2u) | (static_cast(Height - 1) << 14u), + 0xfacu | (lastLevel << 16u) | (Type2D << 28u), + 0u, + maxMip << 4u, + 0u, + 0u, + }; +} + +using Samples = std::vector>; + +Samples Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, const std::vector& coordinates) { + Samples samples; + for (std::size_t first = 0; first < coordinates.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto coordinate = first + lane < coordinates.size() ? coordinates[first + lane] : Coordinate{0.5f, 0.5f}; + Buffer[lane * Words] = std::bit_cast(coordinate.u); + Buffer[lane * Words + 1u] = std::bit_cast(coordinate.v); + } + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads && first + lane < coordinates.size(); ++lane) { + std::array values{}; + std::copy_n(&Buffer[lane * Words + ResultWord], Results, values.begin()); + samples.push_back(values); + } + } + return samples; +} + +std::array Fetch(const SamplerCase& sampler, std::int32_t x, std::int32_t y) { + if (sampler.border && (x < 0 || x >= Width || y < 0 || y >= Height)) return {0.0, 0.0, 0.0, 0.0}; + return TexelOf(std::clamp(x, 0, Width - 1), std::clamp(y, 0, Height - 1)); +} + +std::array Reference(const SamplerCase& sampler, const Coordinate& coordinate) { + if (!sampler.linear) return Fetch(sampler, static_cast(std::floor(coordinate.u)), static_cast(std::floor(coordinate.v))); + const double u = static_cast(coordinate.u) - 0.5; + const double v = static_cast(coordinate.v) - 0.5; + const auto i = static_cast(std::floor(u)); + const auto j = static_cast(std::floor(v)); + const double alpha = u - i; + const double beta = v - j; + std::array result{}; + for (std::uint32_t component = 0; component < 4u; ++component) { + result[component] = (1.0 - alpha) * (1.0 - beta) * Fetch(sampler, i, j)[component] + alpha * (1.0 - beta) * Fetch(sampler, i + 1, j)[component] + + (1.0 - alpha) * beta * Fetch(sampler, i, j + 1)[component] + alpha * beta * Fetch(sampler, i + 1, j + 1)[component]; + } + return result; +} + +void Check(const SamplerCase& sampler, const char* image, const std::vector& coordinates, const Samples& samples) { + Require(samples.size() == coordinates.size(), "image sample unnormalized: a dispatch lost samples"); + const double tolerance = sampler.linear ? 0.5 : 0.25; + for (std::size_t index = 0; index < coordinates.size(); ++index) { + const auto expected = Reference(sampler, coordinates[index]); + for (std::uint32_t instruction = 0; instruction < Instructions.size(); ++instruction) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const double value = static_cast(std::bit_cast(samples[index][instruction * 4u + component])) * 255.0; + Require(std::fabs(value - expected[component]) <= tolerance, std::string(Instructions[instruction]) + ", " + sampler.name + ", " + image + ": (" + std::to_string(coordinates[index].u) + ", " + std::to_string(coordinates[index].v) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } + } +} + +void ExpectFailure(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, std::string_view reason, const char* what) { + const std::vector one{{0.5f, 0.5f}}; + try { + static_cast(Run(device, texture, sampler, one)); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string(what) + ": unexpected error: " + error.what()); + return; + } + throw std::runtime_error(std::string(what) + " was accepted"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(SingleLevel, 1); + FillTexture(MultiLevel, StorageLevels); + const auto coordinates = Coordinates(); + const auto single = TextureDescriptor(SingleLevel.data(), 0u, 0u); + const auto multi = TextureDescriptor(MultiLevel.data(), 0u, StorageLevels - 1u); + for (const auto& sampler : Samplers) { + const auto singleSamples = Run(*device, single, sampler.words, coordinates); + Check(sampler, "1-level image", coordinates, singleSamples); + const auto multiSamples = Run(*device, multi, sampler.words, coordinates); + Check(sampler, "first level of a 4-level image", coordinates, multiSamples); + Require(multiSamples == singleSamples, std::string(sampler.name) + ": the first level of a 4-level image does not sample like a 1-level image"); + } + ExpectFailure(*device, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view"); + ExpectFailure(*device, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); + ExpectFailure(*device, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); + std::puts("image sample unnormalized tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 13c13fcf2..beb406829 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_INTERMEDIATEREPRESENTATION_INCLUDE_INTERMEDIATEREPRESENTATION_IRMETADATA_RESOURCES_HPP #include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include "IntermediateRepresentation/IrOpcode.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include @@ -84,11 +85,27 @@ struct ImageResource { bool operator==(const ImageResource& other) const = default; }; +enum SamplerUse : std::uint8_t { + SamplerUseExplicitLod = 1u << 0u, + SamplerUseImplicitLod = 1u << 1u, + SamplerUseGradient = 1u << 2u, + SamplerUseOffset = 1u << 3u, + SamplerUseCompare = 1u << 4u, + SamplerUseGather = 1u << 5u, + SamplerUseQueryLod = 1u << 6u, + SamplerUseAdjust = 1u << 7u, +}; + +inline bool ImageSampleExplicitLod(std::uint32_t flags, IrShaderStage stage) { + return (flags & (RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLod | RdnaImageSampleFlagLevelZero)) != 0u || stage != IrShaderStage::Pixel; +} + struct SamplerResource { std::uint32_t source = 0; std::uint32_t firstUsePc = 0; bool forcePointFiltering = false; bool depthCompare = false; + std::uint8_t uses = 0; bool operator==(const SamplerResource& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 2b06d89d7..d88d1cb35 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -179,6 +179,67 @@ std::vector GuestSamplersDescriptor(const std::vector samplers; + std::vector images; +}; + +UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) { + UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size())}; + for (std::uint32_t r = 0; r < info.samplers.size(); r++) { + if ((GuestSamplersDescriptor({r}, snapshot)[0] & ForceUnnormalizedBit) == 0u) { + continue; + } + const auto& sampler = info.samplers[r]; + const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod); + if (unsupported != 0u) { + failUnnormalized(UnnormalizedUseReason(unsupported)); + } + if (sampler.depthCompare) { + failUnnormalized("is used with depth comparison"); + } + for (const auto& pair : info.sampledPairs) { + if (pair.sampler != r) { + continue; + } + const auto& image = info.images.at(pair.image); + if (image.indirectRoot != ImageResource::NoIndirectImage) { + failUnnormalized("samples an image selected at run time"); + } + if ((image.dimension != RdnaImageDimension::Dim1D && image.dimension != RdnaImageDimension::Dim2D) || image.cube) { + failUnnormalized("samples a 1D-array, 2D-array, 3D, cube or multisampled image"); + } + if (image.depthCompare) { + failUnnormalized("is used with depth comparison"); + } + if (image.conversionFormat != IrBufferFormat::Invalid || image.packed) { + failUnnormalized("samples an image that needs a format conversion or packed access"); + } + proof.images[pair.image] = true; + } + proof.samplers[r] = true; + } + return proof; +} + std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) { @@ -206,6 +267,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const IrBindingLayout& layout = allocation.layout; const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); + const UnnormalizedProof unnormalized = ProveUnnormalized(info, snapshot); std::vector bindings; bindings.reserve(layout.descriptors.size()); @@ -242,6 +304,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageWritten.push_back(image.written || image.atomic); physical.imageDepthCompare.push_back(image.depthCompare); physical.imageAtomic.push_back(image.atomic); + physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); } break; case DescriptorRole::GuestSamplers: @@ -249,6 +312,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con for (std::size_t element = 0; element < logical.resources.size(); ++element) { const auto& sampler = info.samplers.at(logical.resources[element]); physical.samplerDepthCompare.push_back(sampler.depthCompare); + physical.samplerUnnormalized.push_back(unnormalized.samplers.at(logical.resources[element])); if (sampler.forcePointFiltering) { auto& filter = physical.guestDescriptor.at(element * 4u + 2u); const bool mipmapped = ((filter >> 26u) & 3u) != 0u; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 4fb2a3d89..e8a2c6ec0 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -424,6 +424,50 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh } } +std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage stage) { + if (opcode == IrOpcode::ImageGatherRaw) { + return SamplerUseGather; + } + if (opcode == IrOpcode::ImageQueryLod) { + return SamplerUseQueryLod; + } + std::uint32_t use = (flags & RdnaImageSampleFlagDerivative) != 0u ? SamplerUseGradient : ImageSampleExplicitLod(flags, stage) ? SamplerUseExplicitLod : SamplerUseImplicitLod; + if ((flags & RdnaImageSampleFlagOffset) != 0u) { + use |= SamplerUseOffset; + } + if ((flags & RdnaImageSampleFlagCompare) != 0u) { + use |= SamplerUseCompare; + } + if ((flags & RdnaImageSampleFlagAdjust) != 0u) { + use |= SamplerUseAdjust; + } + return static_cast(use); +} + +void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { + for (auto& sampler : info.samplers) { + sampler.uses = 0; + } + const auto& memoryInfo = program.Resources().memoryInfo; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (!ImageOpcodeInfoOf(inst->Opcode()).needsSampler) { + continue; + } + const auto index = inst->Flags().index; + if (index >= memoryInfo.size()) { + return Fail("image instruction has no memory metadata"); + } + const auto& memory = memoryInfo[index]; + if (memory.sampler >= info.samplers.size()) { + return Fail("image instruction references an unknown sampler"); + } + auto& sampler = info.samplers[memory.sampler]; + sampler.uses = static_cast(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage)); + } + } +} + } void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo& inputInfo) const { @@ -460,6 +504,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + CollectSamplerUses(program, next); program.Info() = std::move(next); program.Metadata().shaderInfoComplete = true; } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 1b3aff3a7..e3d69389d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -328,6 +328,8 @@ struct DescriptorBinding { // skip the write-back and the pending-write note for the element; an element beyond the vector // (a producer that does not fill it) must be treated as written. std::vector bufferWritten; + std::vector samplerUnnormalized; + std::vector imageUnnormalized; }; struct VertexAttribute { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 18711d1e0..d060071fd 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,7 +64,7 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(RecompileResult) == 176, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 304, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 384, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 28, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder"); @@ -251,6 +251,8 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageAtomic); writer.Flags(binding.bufferAtomic); writer.Flags(binding.bufferWritten); + writer.Flags(binding.samplerUnnormalized); + writer.Flags(binding.imageUnnormalized); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { @@ -270,6 +272,8 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.imageAtomic); reader.Flags(binding.bufferAtomic); reader.Flags(binding.bufferWritten); + reader.Flags(binding.samplerUnnormalized); + reader.Flags(binding.imageUnnormalized); } void encodeResult(Writer& writer, const RecompileResult& result) { @@ -419,6 +423,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(sampler.firstUsePc); out.Value(sampler.forcePointFiltering); out.Value(sampler.depthCompare); + out.Value(sampler.uses); }); writer.List(info.sampledPairs, [](Writer& out, const SampledResourcePair& pair) { out.Value(pair.image); @@ -504,11 +509,12 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.indirectSearchIterations); in.Values(image.indirectResources); }); - reader.List(info.samplers, 10, [](Reader& in, SamplerResource& sampler) { + reader.List(info.samplers, 11, [](Reader& in, SamplerResource& sampler) { in.Value(sampler.source); in.Value(sampler.firstUsePc); in.Value(sampler.forcePointFiltering); in.Value(sampler.depthCompare); + in.Value(sampler.uses); }); reader.List(info.sampledPairs, 12, [](Reader& in, SampledResourcePair& pair) { in.Value(pair.image); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 84c7a081b..3abc5c257 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 4; +inline constexpr std::uint32_t FormatVersion = 5; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 419bf78ed..2bdf52114 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1012,7 +1012,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con EmitEmulatedCompareSample(ctx, access, setup); return; } - const bool explicitLod = HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod) || HasFlag(mem, RdnaImageSampleFlagLevelZero) || state.program.Resources().stage != IrShaderStage::Pixel; + const bool explicitLod = ImageSampleExplicitLod(mem.imageSampleFlags, state.program.Resources().stage); std::uint32_t opcode = spv::OpImageSampleImplicitLod; if (explicitLod) { opcode = setup.dref ? spv::OpImageSampleDrefExplicitLod : spv::OpImageSampleExplicitLod; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dade5a9e4..9c9b9d8f8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -127,6 +127,9 @@ Throughout the project, every function at every stage either **does exactly what - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. +- [Unnormalized S# coordinates](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. +- [Unnormalized S#s](../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without offset, comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. +- An [unnormalized S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). From a2e0be6e76d74aed85c55d69e507c3e76564c975 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:28:50 +0200 Subject: [PATCH 031/203] docs: record how EXEC written as a value is read on narrow subgroups On a host subgroup narrower than 32 lanes, threadBit reads an EXEC written with s_mov_b32 or s_mov_b64 at bit LaneId & 31 of EXEC_LO, and LaneId is the subgroup invocation id. Every lane at or above the host subgroup size therefore reads the bit of a lower lane, in wave32 and wave64 alike. An EXEC produced by a compare stays consistent because its ballot is subgroup-local as well. Found while investigating the llvmpipe failure of #751. --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..05cb0acde 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -134,6 +134,7 @@ Throughout the project, every function at every stage either **does exactly what - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. +- On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. From 52869f0112d2ccab6fab58eff42d6e403b095e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 01:28:28 +0200 Subject: [PATCH 032/203] feat(ngs2): custom submixer rack with UserFx2 modules Co-Authored-By: Claude --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 3 +- .../libSceNgs2.native/include/Ngs2Types.hpp | 121 +++++++++ .../libs/prx/libSceNgs2.native/src/Custom.cpp | 109 +++++++++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 23 ++ core/libs/prx/libSceNgs2.native/src/Rack.cpp | 7 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 1 + core/libs/prx/libSceNgs2.native/src/Voice.cpp | 10 +- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 231 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 9 files changed, 499 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/src/Custom.cpp create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 986014221..a2d50f9b8 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceNgs2.native SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/Atrac9.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Custom.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Render.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/System.cpp @@ -25,7 +26,7 @@ if(MINGW) endif() if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 6c99f3f42..e7d5fd04c 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -18,6 +18,7 @@ static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0 static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_MASTERING = 0x3000; +static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32L = 0x18; @@ -50,6 +51,12 @@ static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER = 0x1000000a; static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP = 0x20000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; + +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET = 4; @@ -149,6 +156,104 @@ struct Ngs2MasteringRackOption { std::uint32_t num_peak_meter_blocks; }; +struct Ngs2CustomModuleOption { + std::uint32_t size; +}; + +struct Ngs2UserFx2SetupContext { + void* common; + void* param; + void* work; + std::uintptr_t user_data; + std::uint32_t max_voices; + std::uint32_t voice_index; + std::uint64_t reserved[4]; +}; +static_assert(sizeof(Ngs2UserFx2SetupContext) == 72); + +using Ngs2UserFx2CleanupContext = Ngs2UserFx2SetupContext; + +struct Ngs2UserFx2ControlContext { + const void* data; + std::size_t data_size; + void* common; + void* param; + std::uintptr_t user_data; + std::uint64_t reserved[4]; +}; +static_assert(sizeof(Ngs2UserFx2ControlContext) == 72); + +struct Ngs2UserFx2ProcessContext { + float** channel_data; + void* common; + const void* param; + void* work; + void* state; + std::uintptr_t user_data; + std::uint32_t flags; + std::uint32_t num_input_channels; + std::uint32_t num_output_channels; + std::uint32_t num_grain_samples; + std::uint32_t sample_rate; + std::uint32_t reserved; + std::uint64_t reserved2[4]; +}; +static_assert(sizeof(Ngs2UserFx2ProcessContext) == 104); + +using Ngs2UserFx2SetupHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2SetupContext*); +using Ngs2UserFx2CleanupHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2CleanupContext*); +using Ngs2UserFx2ControlHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2ControlContext*); +using Ngs2UserFx2ProcessHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2ProcessContext*); + +struct Ngs2CustomUserFx2ModuleOption { + Ngs2CustomModuleOption custom_module_option; + Ngs2UserFx2SetupHandler setup_handler; + Ngs2UserFx2CleanupHandler cleanup_handler; + Ngs2UserFx2ControlHandler control_handler; + Ngs2UserFx2ProcessHandler process_handler; + std::size_t common_size; + std::size_t param_size; + std::size_t work_size; + std::uintptr_t user_data; +}; +static_assert(sizeof(Ngs2CustomUserFx2ModuleOption) == 72); + +struct Ngs2CustomRackModuleInfo { + const Ngs2CustomModuleOption* option; + std::uint32_t module_id; + std::uint32_t source_buffer_id; + std::uint32_t extra_buffer_id; + std::uint32_t dest_buffer_id; + std::uint32_t state_offset; + std::uint32_t state_size; + std::uint32_t reserved; + std::uint32_t reserved2; +}; +static_assert(sizeof(Ngs2CustomRackModuleInfo) == 40); + +struct Ngs2CustomRackPortInfo { + std::uint32_t source_buffer_id; + std::uint32_t reserved; +}; + +struct Ngs2CustomRackOption { + Ngs2RackOption rack_option; + std::uint32_t state_size; + std::uint32_t num_buffers; + std::uint32_t num_modules; + std::uint32_t reserved; + Ngs2CustomRackModuleInfo module[SCE_NGS2_CUSTOM_MAX_MODULES]; + Ngs2CustomRackPortInfo port[SCE_NGS2_CUSTOM_MAX_PORTS]; +}; +static_assert(sizeof(Ngs2CustomRackOption) == 1280); + +struct Ngs2CustomSubmixerRackOption { + Ngs2CustomRackOption custom_rack_option; + std::uint32_t max_channels; + std::uint32_t max_inputs; +}; +static_assert(sizeof(Ngs2CustomSubmixerRackOption) == 1288); + struct Ngs2VoiceParamHeader { std::uint16_t size; std::int16_t next; @@ -275,6 +380,22 @@ struct Ngs2SubmixerVoiceSetupParam { }; static_assert(sizeof(Ngs2SubmixerVoiceSetupParam) == 16); +struct Ngs2CustomSubmixerVoiceSetupParam { + Ngs2VoiceParamHeader header; + std::uint32_t num_input_channels; + std::uint32_t num_output_channels; + std::uint32_t flags; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2CustomSubmixerVoiceSetupParam) == 24); + +struct Ngs2CustomVoiceUserFx2Param { + Ngs2VoiceParamHeader header; + const void* data; + std::size_t data_size; +}; +static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); + struct Ngs2MasteringVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_io_channels; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp new file mode 100644 index 000000000..2ba302fb2 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" +#include "Ngs2Internal.hpp" + +static constexpr std::uint32_t USER_FX2_FLAG_PARAM_CHANGED = 2; + +void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { + if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); + if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX; + for (std::uint32_t m = 0; m < option.num_modules; m++) { + const auto& info = option.module[m]; + if (info.module_id != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + } + for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { + if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; + } +} + +static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, std::uint32_t voiceIndex) { + auto& fx = rack.voices[voiceIndex].userFx[module]; + return {rack.userFxCommon[module].data(), fx.param.data(), fx.work.data(), rack.userFx[module].user_data, + static_cast(rack.voices.size()), voiceIndex, {}}; +} + +void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.userFx.resize(option.num_modules); + rack.userFxCommon.resize(option.num_modules); + for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); + for (std::uint32_t m = 0; m < option.num_modules; m++) { + const auto& module = *reinterpret_cast(option.module[m].option); + rack.userFx[m] = module; + rack.userFxCommon[m].resize(module.common_size); + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + auto& fx = rack.voices[i].userFx[m]; + fx.param.resize(module.param_size); + fx.work.resize(module.work_size); + fx.state.resize(option.module[m].state_size); + if (module.setup_handler == nullptr) continue; + auto context = UserFxContext(rack, m, i); + const int result = module.setup_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 setup handler failed with " + std::to_string(result)); + } + } +} + +void Ngs2CleanupUserFx(Ngs2Rack& rack) { + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + for (std::size_t m = 0; m < rack.userFx.size(); m++) { + if (rack.userFx[m].cleanup_handler == nullptr) continue; + auto context = UserFxContext(rack, m, i); + const int result = rack.userFx[m].cleanup_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 cleanup handler failed with " + std::to_string(result)); + } + } +} + +static void SetupCustomSubmixer(Ngs2Voice& voice, const Ngs2CustomSubmixerVoiceSetupParam& setup) { + if (setup.flags != 0) throw std::runtime_error("NGS2: custom submixer setup flags are not implemented"); + if (setup.num_input_channels != setup.num_output_channels) throw std::runtime_error("NGS2: custom submixer channel conversion is not implemented"); + if (setup.num_input_channels == 0 || setup.num_input_channels > voice.rack->maxChannels) APS5_INVALID_ARG_EX; + voice.channels = setup.num_input_channels; +} + +static void SetUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceUserFx2Param& param) { + if (index >= voice.userFx.size()) APS5_INVALID_ARG_EX; + const auto& module = voice.rack->userFx[index]; + if (module.control_handler != nullptr) throw std::runtime_error("NGS2: the UserFx2 control handler is not implemented"); + auto& fx = voice.userFx[index]; + if (param.data_size != fx.param.size() || (param.data == nullptr && param.data_size != 0)) APS5_INVALID_ARG_EX; + if (param.data_size != 0) std::memcpy(fx.param.data(), param.data, param.data_size); + fx.flags |= USER_FX2_FLAG_PARAM_CHANGED; +} + +void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { + if (voice.rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { + SetupCustomSubmixer(voice, ParamAs(param)); + return; + } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { + SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); + return; + } + throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); +} + +void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { + if (voice.userFx.empty()) return; + std::vector channels(voice.channels); + for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; + for (std::size_t m = 0; m < voice.userFx.size(); m++) { + auto& fx = voice.userFx[m]; + const auto& module = voice.rack->userFx[m]; + Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), + module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; + const int result = module.process_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); + fx.flags = 0; + } +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 615380f59..022be9971 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -71,6 +72,13 @@ struct Ngs2Port { std::int32_t matrix = -1; }; +struct Ngs2UserFx2 { + std::vector param; + std::vector work; + std::vector state; + std::uint32_t flags = 0; +}; + struct Ngs2Voice { Ngs2Rack* rack = nullptr; Ngs2PlayState state = Ngs2PlayState::Empty; @@ -93,6 +101,7 @@ struct Ngs2Voice { std::vector> matrices; std::vector filters; std::uint32_t outputId = 0; + std::vector userFx; std::vector samples; bool rendering = false; bool rendered = false; @@ -111,6 +120,8 @@ struct Ngs2Rack { Ngs2ContextBufferInfo bufferInfo{}; Ngs2BufferAllocator allocator{}; std::vector voices; + std::vector userFx; + std::vector> userFxCommon; }; struct Ngs2System { @@ -125,6 +136,13 @@ struct Ngs2System { std::string Ngs2Hex(std::uint32_t value); std::recursive_mutex& Ngs2Mutex(); Ngs2System* Ngs2FindSystem(Ngs2Handle handle); + +template +const TParam& ParamAs(const Ngs2VoiceParamHeader& param) { + if (param.size < sizeof(TParam)) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " is too small"); + return reinterpret_cast(param); +} + Ngs2Rack* Ngs2FindRack(Ngs2Handle handle); Ngs2Voice* Ngs2FindVoice(Ngs2Handle handle); void* Ngs2Place(const Ngs2ContextBufferInfo* bufferInfo, std::size_t size, std::size_t alignment); @@ -134,6 +152,11 @@ void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format); std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block); void Ngs2RestartAtrac9(Ngs2Voice& voice); const float* Ngs2Atrac9Frame(Ngs2Voice& voice, Ngs2Block& block, std::uint32_t frame); +void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option); +void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); +void Ngs2CleanupUserFx(Ngs2Rack& rack); +void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); +void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index b98f15b96..1c0ce54f9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -16,6 +16,7 @@ union RackOptions { Ngs2SamplerRackOption sampler; Ngs2SubmixerRackOption submixer; Ngs2MasteringRackOption mastering; + Ngs2CustomSubmixerRackOption customSubmixer; }; static std::size_t RackOptionSize(std::uint32_t rackId) { @@ -23,11 +24,13 @@ static std::size_t RackOptionSize(std::uint32_t rackId) { case SCE_NGS2_RACK_ID_SAMPLER: return sizeof(Ngs2SamplerRackOption); case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); + case SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER: return sizeof(Ngs2CustomSubmixerRackOption); default: throw std::runtime_error("NGS2: rack id " + Ngs2Hex(rackId) + " is not implemented"); } } static RackOptions DefaultRackOption(std::uint32_t rackId) { + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: default custom rack options are not implemented"); RackOptions options{}; auto& common = options.common; common.size = RackOptionSize(rackId); @@ -69,6 +72,7 @@ static RackOptions DefaultRackOption(std::uint32_t rackId) { static std::uint32_t RackMaxChannels(std::uint32_t rackId, const RackOptions& options) { if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) return options.submixer.max_channels; if (rackId == SCE_NGS2_RACK_ID_MASTERING) return options.mastering.max_channels; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) return options.customSubmixer.max_channels; return NGS2_MAX_CHANNELS; } @@ -82,6 +86,7 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); return options; } @@ -102,12 +107,14 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO voice.ports.resize(options.common.max_ports); voice.matrices.resize(options.common.max_matrices); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2SetupUserFx(*rack, options.customSubmixer.custom_rack_option); system->racks.push_back(rack); *handle = reinterpret_cast(rack); return SCE_NGS2_OK; } int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo) { + Ngs2CleanupUserFx(rack); auto& racks = rack.system->racks; std::erase(racks, &rack); for (auto* other : racks) { diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index ba5b520b3..4d0f5e394 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -176,6 +176,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate); else MixInputs(voice, voices, grain, systemRate); + if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); } voice.rendering = false; voice.rendered = true; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index b92ece2f4..9afecf10c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -69,12 +69,6 @@ const std::uint8_t* Ngs2Voice::WaveformData() const { return block.data + (static_cast(block.info.num_skip_samples) + block.cursor) * channels * sizeof(std::int16_t); } -template -static const TParam& ParamAs(const Ngs2VoiceParamHeader& param) { - if (param.size < sizeof(TParam)) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " is too small"); - return reinterpret_cast(param); -} - static Ngs2Port& PortAt(Ngs2Voice& voice, std::uint32_t port) { if (port >= voice.ports.size()) throw std::invalid_argument("NGS2: voice port " + std::to_string(port) + " is out of range"); return voice.ports[port]; @@ -246,6 +240,10 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { ApplyCommonParam(voice, param); return; } + if (rackId == (SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 >> 16) || rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + Ngs2ApplyCustomParam(voice, param); + return; + } if (rackId != voice.rack->rackId) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); switch (rackId) { case SCE_NGS2_RACK_ID_SAMPLER: ApplySamplerParam(voice, param); return; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp new file mode 100644 index 000000000..198b42fe2 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -0,0 +1,231 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include + +struct Calls { + std::vector setupVoices; + std::vector cleanupVoices; + std::vector processFlags; + std::vector order; + int processResult = SCE_NGS2_OK; +}; +static Calls calls; + +static std::int32_t APS5_VABI Setup(Ngs2UserFx2SetupContext* context) { + Require(context->common != nullptr && context->param != nullptr && context->work != nullptr); + Require(context->user_data == 0x77 && context->max_voices == 2); + std::memset(context->work, 0x5a, 8); + *static_cast(context->param) = 1.0f; + *static_cast(context->common) = 0.25f; + calls.setupVoices.push_back(context->voice_index); + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI Cleanup(Ngs2UserFx2CleanupContext* context) { + Require(context->user_data == 0x77 && context->max_voices == 2 && static_cast(context->work)[7] == 0x5a); + calls.cleanupVoices.push_back(context->voice_index); + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI Gain(Ngs2UserFx2ProcessContext* context) { + Require(context->user_data == 0x77 && context->num_input_channels == 2 && context->num_output_channels == 2); + Require(context->num_grain_samples == Grain && context->sample_rate == 48000 && context->state != nullptr); + Require(static_cast(context->work)[0] == 0x5a); + const float gain = *static_cast(context->param); + for (std::uint32_t c = 0; c < 2; c++) { + for (std::uint32_t i = 0; i < Grain; i++) context->channel_data[c][i] *= gain; + } + *static_cast(context->common) = 0.125f; + calls.processFlags.push_back(context->flags); + calls.order.push_back(0); + return calls.processResult; +} + +static std::int32_t APS5_VABI Offset(Ngs2UserFx2ProcessContext* context) { + const float offset = *static_cast(context->common); + for (std::uint32_t i = 0; i < Grain; i++) context->channel_data[1][i] += offset; + calls.order.push_back(1); + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI ControlHandler(Ngs2UserFx2ControlContext*) { + return SCE_NGS2_OK; +} + +static Ngs2CustomUserFx2ModuleOption Module(Ngs2UserFx2ProcessHandler process) { + return {{sizeof(Ngs2CustomUserFx2ModuleOption)}, Setup, Cleanup, nullptr, process, sizeof(float), sizeof(float), 8, 0x77}; +} + +static Ngs2CustomSubmixerRackOption RackOption(const Ngs2CustomUserFx2ModuleOption* modules, std::uint32_t count) { + Ngs2CustomSubmixerRackOption option{}; + auto& common = option.custom_rack_option.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = count; + for (std::uint32_t m = 0; m < count; m++) { + option.custom_rack_option.module[m].option = &modules[m].custom_module_option; + option.custom_rack_option.module[m].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; + option.custom_rack_option.module[m].state_size = 16; + } + option.max_channels = 2; + option.max_inputs = 1; + return option; +} + +template +static bool CreateThrows(const Ngs2CustomSubmixerRackOption& option) { + Ngs2ContextBufferInfo query{}; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query); + } catch (const TException&) { + return true; + } + return false; +} + +template +static bool ControlThrows(uintptr_t voice, std::uint32_t id, TParam param) { + param.header = {static_cast(sizeof(TParam)), 0, id}; + try { + sceNgs2VoiceControl(voice, ¶m.header); + } catch (const TException&) { + return true; + } + return false; +} + +static uintptr_t CustomRack(uintptr_t system, const Ngs2CustomSubmixerRackOption& option) { + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + return rack; +} + +static uintptr_t Sampler(uintptr_t system, const std::vector& pcm) { + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size() / 2), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + return voice; +} + +static std::vector Render(uintptr_t system) { + std::vector out(Grain * 2, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + return out; +} + +static void TestUserFxChain() { + calls = {}; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomUserFx2ModuleOption modules[2] = {Module(Gain), Module(Offset)}; + const auto rack = CustomRack(system, RackOption(modules, 2)); + Require(calls.setupVoices == (std::vector{0, 1, 0, 1})); + + const auto custom = Voice(rack); + Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(custom, master); + Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector pcm(Grain * 2 * 8, 16384); + const auto sampler = Sampler(system, pcm); + Patch(sampler, custom); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + auto out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + Require(calls.processFlags == (std::vector{0}) && calls.order == (std::vector{0, 1})); + + const float gain = 0.5f; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)}); + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); + Require(calls.processFlags == (std::vector{0, 2})); + out = Render(system); + Require(calls.processFlags == (std::vector{0, 2, 0})); + + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain) - 1})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 2, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 1, 0, 0})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 1, 0})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 3, 3, 0, 0})); + Require(ControlThrows(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); + + calls.processResult = -1; + bool failed = false; + try { + Render(system); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); + calls.processResult = SCE_NGS2_OK; + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(calls.cleanupVoices == (std::vector{0, 0, 1, 1})); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRackOptions() { + calls = {}; + const auto system = CreateSystem(); + const Ngs2CustomUserFx2ModuleOption modules[1] = {Module(Gain)}; + const auto good = RackOption(modules, 1); + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &good.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + + auto option = good; + option.custom_rack_option.num_buffers = 2; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.module[0].module_id = 0x10; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.module[0].dest_buffer_id = 1; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.port[0].source_buffer_id = 1; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.num_modules = SCE_NGS2_CUSTOM_MAX_MODULES + 1; + Require(CreateThrows(option)); + auto noProcess = Module(nullptr); + option = RackOption(&noProcess, 1); + Require(CreateThrows(option)); + auto badSize = Module(Gain); + badSize.custom_module_option.size = 8; + option = RackOption(&badSize, 1); + Require(CreateThrows(option)); + bool failed = false; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, nullptr, &query); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed && calls.setupVoices.empty()); + + auto withControl = Module(Gain); + withControl.control_handler = ControlHandler; + const auto voice = Voice(CustomRack(system, RackOption(&withControl, 1))); + const float gain = 0.5f; + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + Require(calls.cleanupVoices == (std::vector{0, 1})); +} + +int main() { + TestUserFxChain(); + TestRackOptions(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..2cf3d0b50 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -103,6 +103,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted +- [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flag 2 after a parameter change and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. From 5a031f3ef6d7f9233e14b74d3b947a228202b469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:17:23 +0200 Subject: [PATCH 033/203] fix(ngs2): start UserFx2 process flags at 1 on the first call --- core/libs/prx/libSceNgs2.native/src/Custom.cpp | 2 ++ core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp | 6 +++--- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 2ba302fb2..d97ae633c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" +static constexpr std::uint32_t USER_FX2_FLAG_FIRST_PROCESS = 1; static constexpr std::uint32_t USER_FX2_FLAG_PARAM_CHANGED = 2; void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { @@ -44,6 +45,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { fx.param.resize(module.param_size); fx.work.resize(module.work_size); fx.state.resize(option.module[m].state_size); + fx.flags = USER_FX2_FLAG_FIRST_PROCESS; if (module.setup_handler == nullptr) continue; auto context = UserFxContext(rack, m, i); const int result = module.setup_handler(&context); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 198b42fe2..b5872f457 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -145,15 +145,15 @@ static void TestUserFxChain() { auto out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); - Require(calls.processFlags == (std::vector{0}) && calls.order == (std::vector{0, 1})); + Require(calls.processFlags == (std::vector{1}) && calls.order == (std::vector{0, 1})); const float gain = 0.5f; Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)}); out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); - Require(calls.processFlags == (std::vector{0, 2})); + Require(calls.processFlags == (std::vector{1, 2})); out = Render(system); - Require(calls.processFlags == (std::vector{0, 2, 0})); + Require(calls.processFlags == (std::vector{1, 2, 0})); Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain) - 1})); Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 2, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2cf3d0b50..4b4d773ca 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -103,7 +103,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted -- [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flag 2 after a parameter change and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack +- [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. From 6d2713cbdd0f93a3e13fcdfcc24599e1c839876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:44:18 +0200 Subject: [PATCH 034/203] feat(libkernel): implement sceKernelGetDirectMemoryType Co-Authored-By: Claude --- .../prx/libkernel/DirectMemory/Export.cpp | 12 +++++++ core/libs/tests/GuestMemory.cpp | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 373de6c2a..2ab677f2e 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -148,6 +148,18 @@ int APS5_VABI sceKernelDirectMemoryQuery(int64_t offset, int flags, void* info, return 0; } +int APS5_VABI sceKernelGetDirectMemoryType(int64_t offset, int* memory_type, int64_t* start, int64_t* end) { + if (!memory_type || !start || !end) return SCE_KERNEL_ERROR_EINVAL; + int64_t blockStart = 0; + int64_t blockEnd = 0; + int blockType = 0; + if (!DirectMemoryFind(offset, false, &blockStart, &blockEnd, &blockType)) return SCE_KERNEL_ERROR_ENOENT; + *memory_type = blockType; + *start = blockStart; + *end = blockEnd; + return 0; +} + size_t APS5_VABI sceKernelGetDirectMemorySize(void) { return DIRECT_MEMORY_SIZE; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 58358ae15..1aa3007c9 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -59,6 +59,7 @@ int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest*, std::int32_t, int APS5_VABI sceKernelAioWaitRequest(std::int32_t, std::int32_t*, std::uint32_t*); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*); int APS5_VABI sceKernelMlock_nid_postfix(void*, std::uint64_t); +int APS5_VABI sceKernelGetDirectMemoryType(std::int64_t, int*, std::int64_t*, std::int64_t*); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -191,6 +192,38 @@ static void CheckReleaseDirectMemoryClearsMappings() { Require(sceKernelMunmap(mapped, page * 2) == 0); } +static void CheckGetDirectMemoryType() { + constexpr std::size_t page = 0x4000; + std::int64_t first = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 3, &first) == 0); + std::int64_t second = 0; + Require(sceKernelAllocateDirectMemory(first + page * 2, first + page * 3, page, 0, 1, &second) == 0 && second == first + page * 2); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(first, &type, &start, &end) == 0); + Require(type == 3 && start == first && end == first + page * 2); + type = -1; + Require(sceKernelGetDirectMemoryType(first + page * 2 - 1, &type, &start, &end) == 0); + Require(type == 3 && start == first && end == first + page * 2); + Require(sceKernelGetDirectMemoryType(first + page * 2, &type, &start, &end) == 0); + Require(type == 1 && start == second && end == second + page); + Require(sceKernelGetDirectMemoryType(first, nullptr, &start, &end) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelGetDirectMemoryType(first, &type, nullptr, &end) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelGetDirectMemoryType(first, &type, &start, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(first, page) == 0); + type = -1; + start = -1; + end = -1; + Require(sceKernelGetDirectMemoryType(first, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); + Require(type == -1 && start == -1 && end == -1); + Require(sceKernelGetDirectMemoryType(-1, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelGetDirectMemoryType(first + page, &type, &start, &end) == 0); + Require(type == 3 && start == first + page && end == first + page * 2); + Require(sceKernelReleaseDirectMemory(first + page, page * 2) == 0); + Require(sceKernelGetDirectMemoryType(second, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); +} + static void CheckFixedVirtualReservation() { constexpr std::size_t page = 0x4000; void* probe = nullptr; @@ -727,6 +760,7 @@ int main() { CheckFixedVirtualReservation(); CheckMlock(); CheckSharedDirectMemoryLifecycle(); + CheckGetDirectMemoryType(); CheckHeapAfterMappingReuse(); #ifdef _WIN32 CheckNoOverwriteRejectsHostOccupiedMapping(); From 82488a5665a5e0bdc14671d274fa422cf59ec2b1 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Fri, 2 Oct 2026 18:55:56 +0300 Subject: [PATCH 035/203] feat(libSceJson2): implement typed construction, indexed and keyed value access, InitParameter2 Value(ValueType) constructs a value of the given type. getValue(index) and getValue(String) return the element or member, or the null-access result when it is missing, as operator[] does. referValue(index) returns the mutable array element, or null when the value is not an array or the index is out of range, as referValue(String) does for members. Object::size returns the member count. InitParameter2 is the class form of InitParameter: its constructor zeroes it, setAllocator and setFileBufferSize record their arguments, and Initializer::initialize(InitParameter2) succeeds as initialize(InitParameter) does. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceJson2/Export.cpp | 47 ++++++++++++++++++++ core/libs/tests/GuestJson.cpp | 66 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index a19fc3289..8d658e55f 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -567,6 +567,7 @@ void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { (void)se ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { ++self->it; return self; } Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } +std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value* self, bool value) { Construct(*self); self->node->type = TypeBoolean; self->node->boolean = value; } @@ -691,6 +692,25 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t in } return &NullAccess(TypeNull, self); } +void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value* self, ValueType type) { + Construct(*self); + SetType(*self->node, type); +} +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value* self, std::size_t index) { return _ZNK3sce4Json5ValueixEm(self, index); } +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value* self, const String* key) { + const Node& n = NodeOf(*self); + if (key != nullptr && n.type == TypeObject) + for (const auto& pair : *n.object.items) + if (*pair.key.text == *key->text) return &pair.value; + return &NullAccess(TypeNull, self); +} +Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value* self, std::size_t index) { + Node& n = NodeOf(*self); + if (n.type != TypeArray || index >= n.array.items->size()) return nullptr; + auto it = n.array.items->begin(); + std::advance(it, static_cast(index)); + return &*it; +} Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value* self, const String* key) { Node& n = NodeOf(*self); if (key == nullptr || n.type != TypeObject) return nullptr; @@ -747,4 +767,31 @@ int APS5_VABI sceJson2Unknown00(void) { NotImplemented_nid_no_patch("6i18OJSvFWk"); return 0; } + +struct InitParameter2 { + void* allocator; + void* userData; + std::size_t fileBufferSize; +}; +static_assert(sizeof(InitParameter2) <= 40); + +void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(InitParameter2* self, void* allocator, void* userData) { + self->allocator = allocator; + self->userData = userData; +} + +void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(InitParameter2* self, std::size_t size) { + self->fileBufferSize = size; +} + +int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void* self, const InitParameter2* parameter) { + (void)self; + if (parameter == nullptr) throw std::invalid_argument("sce::Json::Initializer::initialize: null parameter"); + return 0; +} + } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index e30b0b123..0874e7cd7 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -71,6 +71,16 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value*, const Str const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value*, std::size_t); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); +const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); +void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value*, std::int32_t); +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value*, std::size_t); +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value*, const String*); +Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value*, std::size_t); +void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(void*); +void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); +void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); +int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); } static void Check(bool value, int line) { @@ -275,9 +285,65 @@ static void NullAccess() { Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } +static void ValueAccess() { + for (std::int32_t type = TypeNull; type <= TypeObject; ++type) { + Value typed{}; + _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&typed, type); + Require(_ZNK3sce4Json5Value7getTypeEv(&typed) == type); + Require(_ZNK3sce4Json5Value5countEv(&typed) == 0); + _ZN3sce4Json5ValueD1Ev(&typed); + } + + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + const char text[] = "{\"list\":[10,20,30],\"name\":\"value\",\"flag\":true}"; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, sizeof(text) - 1) == 0); + Require(_ZNK3sce4Json6Object4sizeEv(_ZNK3sce4Json5Value9getObjectEv(&root)) == 3); + + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "list"); + const Value* list = _ZNK3sce4Json5Value8getValueERKNS0_6StringE(&root, &key); + Require(_ZNK3sce4Json5Value7getTypeEv(list) == TypeArray); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Value8getValueEm(list, 1)) == 20); + Require(_ZNK3sce4Json5Value8getValueEm(list, 2) == _ZNK3sce4Json5ValueixEm(list, 2)); + Require(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Value8getValueEm(list, 3)) == TypeNull); + + Value* mutableList = _ZN3sce4Json5Value10referValueERKNS0_6StringE(&root, &key); + Value* element = _ZN3sce4Json5Value10referValueEm(mutableList, 0); + Require(element != nullptr); + Require(_ZN3sce4Json5Value3setEl(element, 15) == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Value8getValueEm(list, 0)) == 15); + Require(_ZN3sce4Json5Value10referValueEm(mutableList, 3) == nullptr); + Require(_ZN3sce4Json5Value10referValueEm(&root, 0) == nullptr); + _ZN3sce4Json6StringD1Ev(&key); + + String missing{}; + _ZN3sce4Json6StringC1EPKc(&missing, "missing"); + Require(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Value8getValueERKNS0_6StringE(&root, &missing)) == TypeNull); + _ZN3sce4Json6StringD1Ev(&missing); + _ZN3sce4Json5ValueD1Ev(&root); + + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + _ZN3sce4Json14InitParameter2C1Ev(parameter); + int allocator = 0; + int userData = 0; + _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(parameter, &allocator, &userData); + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 4096); + void* stored[3]{}; + std::memcpy(stored, parameter, sizeof(stored)); + Require(stored[0] == &allocator && stored[1] == &userData && reinterpret_cast(stored[2]) == 4096); + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); +} + int main() { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); NullAccess(); + ValueAccess(); } From fcd349fb2bcc717e005377cb84a564e4c97a5ac6 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Mon, 5 Oct 2026 18:40:11 +0300 Subject: [PATCH 036/203] docs: record the assumed InitParameter2 layout in TechnicalDebt Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..0e3af730c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -126,6 +126,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown +- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does ### Functional From 54f0e03cdeef7cab36a3f0ff32c5de87a99adc5f Mon Sep 17 00:00:00 2001 From: Lexroid Date: Mon, 5 Oct 2026 19:12:49 +0300 Subject: [PATCH 037/203] feat(libSceIme): implement sceImeKeyboardGetResourceId The resource id array is cleared and given the user. A null array returns INVALID_ADDRESS and an invalid user INVALID_USER_ID without writing it; a user whose keyboard is not open gets NOT_OPENED, and an open one gets CONNECTION_FAILED, as no USB keyboard is reported through the IME, as in shadPS4. The missing IME keyboard events are recorded in TechnicalDebt. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceIme/Export.cpp | 11 ++++++---- core/libs/tests/GuestImeParameters.cpp | 30 ++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 480dbbd24..b105f365b 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -11,6 +11,7 @@ namespace { constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); @@ -58,10 +59,12 @@ int APS5_VABI sceImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info) { } int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids) { - (void)user_id; - (void)resource_ids; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!resource_ids) return ErrorInvalidAddress; + if (user_id == UserIdInvalid) return ErrorInvalidUserId; + *resource_ids = {}; + resource_ids->user_id = user_id; + std::lock_guard lock(g_keyboardMutex); + return g_openKeyboards.contains(user_id) ? ErrorConnectionFailed : ErrorNotOpened; } int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { diff --git a/core/libs/tests/GuestImeParameters.cpp b/core/libs/tests/GuestImeParameters.cpp index 3612fb077..f7711197d 100644 --- a/core/libs/tests/GuestImeParameters.cpp +++ b/core/libs/tests/GuestImeParameters.cpp @@ -11,6 +11,9 @@ int APS5_VABI sceImeClose_nid_postfix(void); int APS5_VABI sceImeSetCaret(const Caret* caret); int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length); int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry); +int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param); +int APS5_VABI sceImeKeyboardClose(int32_t user_id); +int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids); } static_assert(sizeof(Param) == 96); @@ -118,9 +121,36 @@ static void CheckClosedPanel() { Require(sceImeClose_nid_postfix() == notOpened, "closing needs an open panel"); } +static void CheckKeyboardResourceIds() { + constexpr int NotOpened = static_cast(0x80bc0002u); + constexpr int ConnectionFailed = static_cast(0x80bc0004u); + constexpr int InvalidUserId = static_cast(0x80bc0010u); + constexpr int InvalidAddress = static_cast(0x80bc0031u); + KeyboardResourceIdArray ids; + std::memset(&ids, 0xa5, sizeof(ids)); + Require(sceImeKeyboardGetResourceId(1, nullptr) == InvalidAddress, "null resource id array"); + Require(sceImeKeyboardGetResourceId(-1, &ids) == InvalidUserId, "invalid user"); + const auto* bytes = reinterpret_cast(&ids); + for (size_t i = 0; i < sizeof(ids); ++i) Require(bytes[i] == 0xa5, "outputs changed on argument error"); + Require(sceImeKeyboardGetResourceId(1, &ids) == NotOpened, "keyboard not opened"); + Require(ids.user_id == 1, "user not reported for an unopened keyboard"); + for (uint32_t id : ids.resource_id) Require(id == 0, "resource id reported for an unopened keyboard"); + KeyboardParam param{}; + Require(sceImeKeyboardOpen(1, ¶m) == 0, "keyboard open failed"); + std::memset(&ids, 0xa5, sizeof(ids)); + Require(sceImeKeyboardGetResourceId(1, &ids) == ConnectionFailed, "a keyboard was reported as connected"); + Require(ids.user_id == 1, "user not reported"); + for (uint32_t id : ids.resource_id) Require(id == 0, "resource id reported without a keyboard"); + Require(sceImeKeyboardGetResourceId(2, &ids) == NotOpened, "keyboard of another user reported as opened"); + Require(ids.user_id == 2, "other user not reported"); + Require(sceImeKeyboardClose(1) == 0, "keyboard close failed"); + Require(sceImeKeyboardGetResourceId(1, &ids) == NotOpened, "closed keyboard reported as opened"); +} + int main() { CheckInitialization(); CheckPanelSizes(); CheckErrors(); CheckClosedPanel(); + CheckKeyboardResourceIds(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..3b845ee92 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -166,3 +166,4 @@ Throughout the project, every function at every stage either **does exactly what - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). Lines are allocated on the host, not in the font memory +- [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard From 96499c856081c20081f8cc98500a16a1a3ec1555 Mon Sep 17 00:00:00 2001 From: ebosido444-ai <316167330+ebosido444-ai@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:00:09 +0300 Subject: [PATCH 038/203] fix(tools): count function-try-block definitions in nid_names.py --- tools/nid_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 9697adbcd..2eb43a804 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -19,7 +19,7 @@ CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30]) ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)') -DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?\{") +DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" From 6dbdd927eed09f0b7e12f483c3ac157dd4512c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 08:01:34 +0200 Subject: [PATCH 039/203] feat(ngs2): system sample rate, user data and lock Co-Authored-By: Claude --- .../libSceNgs2.native/src/Ngs2Internal.hpp | 1 + .../libs/prx/libSceNgs2.native/src/System.cpp | 49 +++++++++++++ .../libSceNgs2.native/tests/Ngs2Filter.cpp | 18 +++++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 72 +++++++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 7 +- docs/dev/TechnicalDebt.md | 1 + 6 files changed, 147 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 615380f59..dd2dd3cc7 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -119,6 +119,7 @@ struct Ngs2System { Ngs2BufferAllocator allocator{}; std::uint32_t uid = 0; std::int64_t renderCount = 0; + std::uintptr_t userData = 0; std::vector racks; }; diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 1bb850606..468dcc39a 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -188,6 +188,55 @@ int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num return SCE_NGS2_OK; } +int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sample_rate) { + std::lock_guard lock(Ngs2Mutex()); + auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (sample_rate == 0) APS5_INVALID_ARG_EX; + for (const auto* rack : system->racks) { + for (const auto& voice : rack->voices) { + for (const auto& filter : voice.filters) { + if (filter.enabled && sample_rate != system->option.sample_rate) { + throw std::runtime_error("NGS2: changing the sample rate under an enabled sampler filter is not implemented"); + } + } + } + } + system->option.sample_rate = sample_rate; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_data) { + std::lock_guard lock(Ngs2Mutex()); + auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + system->userData = user_data; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_data) { + std::lock_guard lock(Ngs2Mutex()); + const auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (user_data == nullptr) APS5_INVALID_ARG_EX; + *user_data = system->userData; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemLock(uintptr_t system_handle) { + Ngs2Mutex().lock(); + if (Ngs2FindSystem(system_handle) != nullptr) return SCE_NGS2_OK; + Ngs2Mutex().unlock(); + return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; +} + +int APS5_VABI sceNgs2SystemUnlock(uintptr_t system_handle) { + std::lock_guard lock(Ngs2Mutex()); + if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + Ngs2Mutex().unlock(); + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info, uint32_t num_buffer_info) { std::lock_guard lock(Ngs2Mutex()); auto* system = Ngs2FindSystem(system_handle); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp index 0c7f193c4..cf47e314b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp @@ -173,10 +173,28 @@ static void TestRejectedParams() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestRateChangeUnderFilter() { + const auto system = CreateSystem(); + const auto sampler = Sampler(system, std::vector(Grain, 0), 1, 48000); + SetFilter(sampler, Filter(0, 1, 1, 0, 1000.0f, 1.0f, 1.0f)); + Require(sceNgs2SystemSetSampleRate(system, 48000) == SCE_NGS2_OK); + bool rejected = false; + try { + sceNgs2SystemSetSampleRate(system, 96000); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); + SetFilter(sampler, Filter(0, 1, 0, 0, 0.0f, 1.0f, 1.0f)); + Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestLowPassAndTail(); TestBypassMaskAndGain(); TestSystemRateAndZeroCutoff(); TestRejectedParams(); + TestRateChangeUnderFilter(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index ea0f7c9eb..5250c7eee 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -1,7 +1,11 @@ #include "Ngs2Test.hpp" +#include +#include #include #include +#include +#include #include static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats) { @@ -152,6 +156,71 @@ static std::int32_t APS5_VABI Release(Ngs2ContextBufferInfo* info) { return SCE_NGS2_OK; } +static void TestSampleRate() { + const auto system = CreateSystem(); + Require(sceNgs2SystemSetSampleRate(0x1234, 96000) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + bool threw = false; + try { + sceNgs2SystemSetSampleRate(system, 0); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); + Ngs2SystemInfo info{}; + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.sample_rate == 96000); + + const auto master = Mastering(system, 1); + const std::vector pcm{0, 1000, 2000, 3000}; + const auto sampler = Sampler(system, pcm, 1); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const std::int16_t expected[Grain] = {0, 500, 1000, 1500, 2000, 2500, 3000, 1500}; + const auto out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == expected[i]); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestUserData() { + uintptr_t value = 1; + Require(sceNgs2SystemSetUserData(0x1234, 5) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Require(sceNgs2SystemGetUserData(0x1234, &value) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE && value == 1); + const auto system = CreateSystem(); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2SystemSetUserData(system, 0xfedcba9876543210) == SCE_NGS2_OK); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0xfedcba9876543210); + bool threw = false; + try { + sceNgs2SystemGetUserData(system, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestLock() { + const auto system = CreateSystem(); + Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Require(sceNgs2SystemUnlock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Ngs2SystemInfo info{}; + std::thread free([&] { Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); }); + free.join(); + + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::atomic done = false; + std::thread blocked([&] { + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); + done = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!done); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + blocked.join(); + Require(done); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestAllocator() { const Ngs2BufferAllocator allocator{Allocate, Release, 9}; uintptr_t system = 0; @@ -171,6 +240,9 @@ int main() { TestPcmBlockEnd(); TestPitchAndRepeat(); TestSubmixerMatrix(); + TestSampleRate(); + TestUserData(); + TestLock(); TestAllocator(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 223618449..0b1429a67 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -18,6 +18,11 @@ int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption*, const Ng int APS5_VABI sceNgs2SystemDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2SystemGetInfo(uintptr_t, Ngs2SystemInfo*, size_t); int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t, uint32_t); +int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t, uint32_t); +int APS5_VABI sceNgs2SystemSetUserData(uintptr_t, uintptr_t); +int APS5_VABI sceNgs2SystemGetUserData(uintptr_t, uintptr_t*); +int APS5_VABI sceNgs2SystemLock(uintptr_t); +int APS5_VABI sceNgs2SystemUnlock(uintptr_t); int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32_t); int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*); @@ -40,7 +45,7 @@ inline void Check(bool value, int line) { inline constexpr std::uint32_t Grain = 8; -inline std::vector buffers[16]; +inline std::vector buffers[32]; inline std::size_t usedBuffers = 0; inline Ngs2ContextBufferInfo Buffer(const Ngs2ContextBufferInfo& query) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..9ff11e7d2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -118,6 +118,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw +- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. From bc7fdff9e5eac79aa5f1680b549acd0b015172fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 08:06:17 +0200 Subject: [PATCH 040/203] feat(libSceHttp): add sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert Co-Authored-By: Claude --- core/libs/prx/libSceHttp/Export.cpp | 16 ++++++++++++++++ core/libs/tests/GuestHttp.cpp | 6 ++++++ docs/dev/TechnicalDebt.md | 2 ++ 3 files changed, 24 insertions(+) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index fa00ad2d7..1d10d5348 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -172,6 +172,12 @@ int APS5_VABI sceHttpSetRecvTimeOut(int id, uint32_t usec) { return 0; } +int APS5_VABI sceHttpSetResponseHeaderMaxSize(int id, uint64_t header_size) { + (void)id; + (void)header_size; + return 0; +} + int APS5_VABI sceHttpSetRequestContentLength(int request_id, uint64_t content_length) { (void)request_id; (void)content_length; @@ -242,6 +248,11 @@ int APS5_VABI sceHttpReadData(int request_id, void* data, size_t size) { return ERROR_NETWORK; } +int APS5_VABI sceHttpRedirectCacheFlush(int http_ctx_id) { + (void)http_ctx_id; + return 0; +} + int APS5_VABI sceHttpSetChunkedTransferEnabled(int id, int enable) { (void)id; (void)enable; @@ -345,6 +356,11 @@ int APS5_VABI sceHttpsLoadCert(int http_ctx_id, int num, void* ca_list, void* ce return 0; } +int APS5_VABI sceHttpsUnloadCert(int http_ctx_id) { + (void)http_ctx_id; + return 0; +} + int APS5_VABI sceHttpGetLastErrno(int request_id, int* errno_out) { (void)request_id; if (errno_out == nullptr) { diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 1a5d01074..ae625c507 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -18,6 +18,9 @@ int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t int APS5_VABI sceHttpsEnableOption(int, std::uint32_t); int APS5_VABI sceHttpsLoadCert(int, int, void*, void*, void*); int APS5_VABI sceHttpGetLastErrno(int, int*); +int APS5_VABI sceHttpSetResponseHeaderMaxSize(int, std::uint64_t); +int APS5_VABI sceHttpRedirectCacheFlush(int); +int APS5_VABI sceHttpsUnloadCert(int); int APS5_VABI sceHttpParseStatusLine(const char*, std::size_t, std::int32_t*, std::int32_t*, std::int32_t*, const char**, std::size_t*); } @@ -137,6 +140,9 @@ int main() { Require(sceHttpSetInflateGZIPEnabled(1, -1) == invalidValue); Require(sceHttpsEnableOption(1, 0) == 0); Require(sceHttpsLoadCert(1, 0, nullptr, nullptr, nullptr) == 0); + Require(sceHttpsUnloadCert(1) == 0); + Require(sceHttpSetResponseHeaderMaxSize(1, 8192) == 0); + Require(sceHttpRedirectCacheFlush(1) == 0); int httpErrno = -1; Require(sceHttpGetLastErrno(1, &httpErrno) == 0); Require(httpErrno == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..9c35efd56 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -26,6 +26,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does +- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate ### Unknown function info @@ -106,6 +107,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. +- [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceHttpSetRequestStatusCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - no reference implements it (shadPS4 only logs and returns 0); when the callback runs and what status it gets is unknown From 0f2699eef4c406573d5f35625d53c83634d0de9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 08:11:07 +0200 Subject: [PATCH 041/203] feat(libkernel): Aio batch submit, wait, cancel and delete Co-Authored-By: Claude --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 142 +++++++++++++++++++++--- core/libs/tests/GuestAio.cpp | 92 +++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 220 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index b592c1643..4811010a6 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -26,6 +27,10 @@ constexpr std::int32_t AioProcessing = 2; constexpr std::int32_t AioCompleted = 3; constexpr std::int32_t AioAborted = 4; constexpr std::int32_t AioMaxQueues = 512; +constexpr std::int32_t AioRequestNumMax = 128; +constexpr std::int32_t AioIdNumMax = 128; +constexpr std::uint32_t AioWaitAnd = 1; +constexpr std::uint32_t AioWaitOr = 2; std::mutex g_mutex; std::array g_states{}; @@ -100,35 +105,67 @@ std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, s #endif } -int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id) { +bool RunRequest(KernelAioRwRequest& req, bool write) { + const std::int64_t done = write + ? NativePwrite(req.fd, req.buf, req.nbyte, req.offset) + : NativePread(req.fd, req.buf, req.nbyte, req.offset); + if (done < 0) { + const int error = errno; + req.result->return_value = static_cast(SCE_KERNEL_ERROR_EIO); + if (error == EBADF) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); + if (error == EFAULT) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); + req.result->state = AioAborted; + return false; + } + req.result->return_value = done; + req.result->state = AioCompleted; + return true; +} + +int ValidateRequests(KernelAioRwRequest* req, std::int32_t size, const void* id) { if (req == nullptr || id == nullptr) return SCE_KERNEL_ERROR_EFAULT; if (size <= 0) return SCE_KERNEL_ERROR_EINVAL; for (std::int32_t i = 0; i < size; ++i) { if (req[i].result == nullptr) return SCE_KERNEL_ERROR_EFAULT; } + return 0; +} + +int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id) { + const int error = ValidateRequests(req, size, id); + if (error != 0) return error; const std::int32_t queue = AllocateId(); bool aborted = false; for (std::int32_t i = 0; i < size; ++i) { - const std::int64_t done = write - ? NativePwrite(req[i].fd, req[i].buf, req[i].nbyte, req[i].offset) - : NativePread(req[i].fd, req[i].buf, req[i].nbyte, req[i].offset); - if (done < 0) { - const int error = errno; - req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EIO); - if (error == EBADF) req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); - if (error == EFAULT) req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); - req[i].result->state = AioAborted; - aborted = true; - } else { - req[i].result->return_value = done; - req[i].result->state = AioCompleted; - } + if (!RunRequest(req[i], write)) aborted = true; } SetState(queue, aborted ? AioAborted : AioCompleted); *id = queue; return 0; } +int SubmitCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id, const char* name) { + const int error = ValidateRequests(req, size, id); + if (error != 0) return error; + if (size > AioRequestNumMax) throw std::runtime_error(std::string(name) + ": more than 128 requests per batch is not modelled"); + for (std::int32_t i = 0; i < size; ++i) { + const std::int32_t queue = AllocateId(); + SetState(queue, RunRequest(req[i], write) ? AioCompleted : AioAborted); + id[i] = queue; + } + return 0; +} + +int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out, const char* name) { + if (id == nullptr || out == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (num < 0) return SCE_KERNEL_ERROR_EINVAL; + if (num > AioIdNumMax) throw std::runtime_error(std::string(name) + ": more than 128 ids per batch is not modelled"); + for (std::int32_t i = 0; i < num; ++i) { + if (!ValidId(id[i])) return SCE_KERNEL_ERROR_EINVAL; + } + return 0; +} + } extern "C" { @@ -141,6 +178,46 @@ int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) { return 0; } +int APS5_VABI sceKernelAioDeleteRequests(int32_t* id, int32_t num, int32_t* ret) { + const int error = ValidateIds(id, num, ret, "sceKernelAioDeleteRequests"); + if (error != 0) return error; + for (int32_t i = 0; i < num; ++i) { + SetState(id[i], AioAborted); + ret[i] = 0; + } + return 0; +} + +int APS5_VABI sceKernelAioCancelRequest(int32_t id, int32_t* state) { + if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (id == 0) { + *state = AioProcessing; + return 0; + } + if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL; + SetState(id, AioAborted); + *state = AioAborted; + return 0; +} + +int APS5_VABI sceKernelAioCancelRequests(int32_t* id, int32_t num, int32_t* state) { + if (id == nullptr || state == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (num < 0) return SCE_KERNEL_ERROR_EINVAL; + if (num > AioIdNumMax) throw std::runtime_error("sceKernelAioCancelRequests: more than 128 ids per batch is not modelled"); + for (int32_t i = 0; i < num; ++i) { + if (id[i] != 0 && !ValidId(id[i])) return SCE_KERNEL_ERROR_EINVAL; + } + for (int32_t i = 0; i < num; ++i) { + if (id[i] == 0) { + state[i] = AioProcessing; + continue; + } + SetState(id[i], AioAborted); + state[i] = AioAborted; + } + return 0; +} + int APS5_VABI sceKernelAioInitializeImpl(void* param, int32_t size) { (void)param; (void)size; @@ -161,6 +238,16 @@ int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, int32_t s return SubmitCommands(req, size, true, id); } +int APS5_VABI sceKernelAioSubmitReadCommandsMultiple(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { + (void)prio; + return SubmitCommandsMultiple(req, size, false, id, "sceKernelAioSubmitReadCommandsMultiple"); +} + +int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { + (void)prio; + return SubmitCommandsMultiple(req, size, true, id, "sceKernelAioSubmitWriteCommandsMultiple"); +} + int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL; @@ -169,6 +256,31 @@ int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state) { return 0; } +int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, uint32_t mode, uint32_t* usec) { + const int error = ValidateIds(id, num, state, "sceKernelAioWaitRequests"); + if (error != 0) return error; + if (mode != AioWaitAnd && mode != AioWaitOr) throw std::runtime_error("sceKernelAioWaitRequests: unsupported wait mode"); + const auto start = std::chrono::steady_clock::now(); + for (;;) { + bool allDone = true; + bool anyCompleted = false; + { + std::lock_guard lock(g_mutex); + for (int32_t i = 0; i < num; ++i) { + state[i] = g_states[id[i]]; + if (state[i] == AioProcessing) allDone = false; + if (state[i] == AioCompleted) anyCompleted = true; + } + } + if (allDone || (mode == AioWaitOr && anyCompleted)) return 0; + if (usec != nullptr && *usec != 0) { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + if (elapsed > static_cast(*usec)) return SCE_KERNEL_ERROR_ETIMEDOUT; + } + std::this_thread::sleep_for(std::chrono::microseconds(10)); + } +} + int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL; diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 36cad273a..7193da905 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); @@ -20,6 +21,12 @@ int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32 int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioPollRequest(std::int32_t id, std::int32_t* state); int APS5_VABI sceKernelAioWaitRequest(std::int32_t id, std::int32_t* state, std::uint32_t* usec); +int APS5_VABI sceKernelAioSubmitReadCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioWaitRequests(std::int32_t* id, std::int32_t num, std::int32_t* state, std::uint32_t mode, std::uint32_t* usec); +int APS5_VABI sceKernelAioCancelRequest(std::int32_t id, std::int32_t* state); +int APS5_VABI sceKernelAioCancelRequests(std::int32_t* id, std::int32_t num, std::int32_t* state); +int APS5_VABI sceKernelAioDeleteRequests(std::int32_t* id, std::int32_t num, std::int32_t* ret); } static void Require(bool value, int line) { if (!value) { @@ -86,5 +93,90 @@ int main() { Check(sceKernelAioPollRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT); Check(sceKernelAioPollRequest(0, &polled) == SCE_KERNEL_ERROR_EINVAL); Check(sceKernelAioDeleteRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT); + const int batchFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(batchFd >= 0); + std::array patch = {'W', 'X', 'Y', 'Z'}; + KernelAioResult patchResult{-1, 0}; + KernelAioResult badResult{-1, 0}; + KernelAioRwRequest writeBatch[2] = { + {8, patch.size(), patch.data(), &patchResult, batchFd}, + {0, patch.size(), patch.data(), &badResult, -1}, + }; + std::int32_t writeIds[2] = {0, 0}; + Check(sceKernelAioSubmitWriteCommandsMultiple(writeBatch, 2, 0, writeIds) == 0); + Check(writeIds[0] > 0 && writeIds[1] > 0 && writeIds[0] != writeIds[1]); + Check(patchResult.state == 3 && patchResult.return_value == 4); + Check(badResult.state == 4 && badResult.return_value == SCE_KERNEL_ERROR_EBADF); + std::int32_t writeStates[2] = {0, 0}; + Check(sceKernelAioWaitRequests(writeIds, 2, writeStates, 1, nullptr) == 0); + Check(writeStates[0] == 3 && writeStates[1] == 4); + std::array readBack{}; + std::array tail{}; + KernelAioResult readBackResult{-1, 0}; + KernelAioResult tailResult{-1, 0}; + KernelAioRwRequest readBatch[2] = { + {8, readBack.size(), readBack.data(), &readBackResult, batchFd}, + {60, tail.size(), tail.data(), &tailResult, batchFd}, + }; + std::int32_t readIds[2] = {0, 0}; + Check(sceKernelAioSubmitReadCommandsMultiple(readBatch, 2, 0, readIds) == 0); + Check(readIds[0] > 0 && readIds[1] > 0 && readIds[0] != readIds[1]); + Check(readBackResult.state == 3 && readBackResult.return_value == 4); + Check(std::memcmp(readBack.data(), "WXYZ", 4) == 0); + Check(tailResult.state == 3 && tailResult.return_value == 4); + Check(std::memcmp(tail.data(), seed.data() + 60, 4) == 0); + std::int32_t readStates[2] = {0, 0}; + std::uint32_t timeout = 1000; + Check(sceKernelAioWaitRequests(readIds, 2, readStates, 2, &timeout) == 0); + Check(readStates[0] == 3 && readStates[1] == 3); + std::int32_t cancelled = 0; + Check(sceKernelAioCancelRequest(readIds[0], &cancelled) == 0); + Check(cancelled == 4); + Check(sceKernelAioPollRequest(readIds[0], &polled) == 0); + Check(polled == 4); + Check(sceKernelAioCancelRequest(0, &cancelled) == 0); + Check(cancelled == 2); + std::int32_t cancelIds[2] = {0, readIds[1]}; + std::int32_t cancelStates[2] = {0, 0}; + Check(sceKernelAioCancelRequests(cancelIds, 2, cancelStates) == 0); + Check(cancelStates[0] == 2 && cancelStates[1] == 4); + Check(sceKernelAioPollRequest(readIds[1], &polled) == 0); + Check(polled == 4); + std::int32_t deletedRets[2] = {-1, -1}; + Check(sceKernelAioDeleteRequests(writeIds, 2, deletedRets) == 0); + Check(deletedRets[0] == 0 && deletedRets[1] == 0); + Check(sceKernelAioPollRequest(writeIds[0], &polled) == 0); + Check(polled == 4); + Check(sceKernelAioDeleteRequests(writeIds, 0, deletedRets) == 0); + Check(sceKernelClose(batchFd) == 0); + std::int32_t badIds[2] = {readIds[0], 9999}; + std::int32_t untouched[2] = {7, 7}; + Check(sceKernelAioSubmitReadCommandsMultiple(nullptr, 1, 0, readIds) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioSubmitReadCommandsMultiple(readBatch, 1, 0, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioSubmitWriteCommandsMultiple(writeBatch, 0, 0, writeIds) == SCE_KERNEL_ERROR_EINVAL); + KernelAioRwRequest noResult{0, 1, readBack.data(), nullptr, batchFd}; + Check(sceKernelAioSubmitWriteCommandsMultiple(&noResult, 1, 0, writeIds) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioWaitRequests(readIds, 2, nullptr, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioWaitRequests(nullptr, 2, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioWaitRequests(readIds, -1, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelAioWaitRequests(badIds, 2, untouched, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Check(untouched[0] == 7 && untouched[1] == 7); + bool threw = false; + try { sceKernelAioWaitRequests(readIds, 2, readStates, 3, nullptr); } catch (const std::runtime_error&) { threw = true; } + Check(threw); + threw = false; + std::array manyIds{}; + std::array manyStates{}; + manyIds.fill(readIds[0]); + try { sceKernelAioDeleteRequests(manyIds.data(), 129, manyStates.data()); } catch (const std::runtime_error&) { threw = true; } + Check(threw); + Check(sceKernelAioCancelRequest(9999, &cancelled) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelAioCancelRequest(readIds[0], nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioCancelRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL); + Check(untouched[0] == 7 && untouched[1] == 7); + Check(sceKernelAioCancelRequests(cancelIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioDeleteRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL); + Check(untouched[0] == 7 && untouched[1] == 7); + Check(sceKernelAioDeleteRequests(writeIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); Check(std::filesystem::remove_all(root) > 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..63b571c21 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -65,6 +65,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed +- [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted - [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws From 57466b6267f316ad0018086b0441cc41450c7d61 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 6 Oct 2026 08:35:25 +0200 Subject: [PATCH 042/203] test(libs): keep the fixture's DT_NEEDED entries under --as-needed Ubuntu's GCC links with --as-needed by default, and the fixture uses nothing from libc or libstdc++, so it ended up with no DT_NEEDED entry and the dynamic string test had no needed names to check. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 083a03738..ba0e4884c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -89,6 +89,7 @@ if(BUILD_TESTING) set_target_properties(guest_module_fixture PROPERTIES BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN") find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND AND NOT WIN32) + target_link_options(guest_module_fixture PRIVATE -Wl,--no-as-needed) add_test(NAME nid_patcher_dynamic_strings COMMAND ${Python3_EXECUTABLE} ${LIBS_INCLUDE_DIR}/nid/tests/test_dynamic_strings.py $) endif() From 5e009d75ec92c2fb1b5982e28e6896436e7988f1 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 19:50:09 +0200 Subject: [PATCH 043/203] feat(libkernel): implement the rtld thread atexit reference count A title's libc.prx passes the dso address of __cxa_thread_atexit to _sceKernelRtldThreadAtexitIncrement and fails the registration when it returns nonzero. Increment takes a reference on the loaded image that contains the address, so it stays mapped while thread_local destructors are pending, and Decrement releases one. An address outside every loaded image and a Decrement without a reference throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Rtld.cpp | 86 +++++++++++++++++---- core/libs/tests/GuestDynamicLoader.cpp | 27 +++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 7a2f27d81..ad3fa2a65 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -2,16 +2,78 @@ #include #include #include +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" +#ifdef _WIN32 +#define NOMINMAX +#include +#else +#include +#include +#endif namespace { std::mutex rtldHeapMutex; std::array rtldHeapApi{}; +std::mutex threadAtexitMutex; +std::multimap threadAtexitReferences; + +const void* imageContaining(const void* address, bool reference, void*& handle, const char* caller) { + handle = nullptr; +#ifdef _WIN32 + const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | (reference ? 0 : GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT); + HMODULE module = nullptr; + if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) + throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (reference) handle = module; + return module; +#else + Dl_info info{}; + link_map* image = nullptr; + if (!dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || !image) + throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (reference && image->l_name && *image->l_name) { + handle = dlopen(image->l_name, RTLD_LAZY | RTLD_NOLOAD); + if (!handle) throw std::runtime_error(std::string(caller) + ": cannot reference " + image->l_name); + } + return image; +#endif +} + +int referenceThreadAtexitImage(const void* address, const char* caller) { + void* handle = nullptr; + const void* image = imageContaining(address, true, handle, caller); + std::lock_guard lock(threadAtexitMutex); + threadAtexitReferences.emplace(image, handle); + return 0; +} + +int releaseThreadAtexitImage(const void* address, const char* caller) { + void* handle = nullptr; + const void* image = imageContaining(address, false, handle, caller); + { + std::lock_guard lock(threadAtexitMutex); + const auto found = threadAtexitReferences.find(image); + if (found == threadAtexitReferences.end()) + throw std::runtime_error(std::string(caller) + ": image holds no thread atexit reference"); + handle = found->second; + threadAtexitReferences.erase(found); + } + if (!handle) return 0; +#ifdef _WIN32 + if (!FreeLibrary(static_cast(handle))) +#else + if (dlclose(handle) != 0) +#endif + throw std::runtime_error(std::string(caller) + ": cannot release image"); + return 0; +} void registerApplicationHeapApi(void* const* api) { if (api == nullptr) @@ -34,16 +96,12 @@ void APS5_VABI sceKernelRtldSetApplicationHeapAPI(void* api[]) { registerApplicationHeapApi(api); } -int APS5_VABI sceKernelRtldThreadAtexitDecrement(uint64_t* c) { - (void)c; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelRtldThreadAtexitDecrement(const void* dsoSymbol) { + return releaseThreadAtexitImage(dsoSymbol, __func__); } -int APS5_VABI sceKernelRtldThreadAtexitIncrement(uint64_t* c) { - (void)c; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelRtldThreadAtexitIncrement(const void* dsoSymbol) { + return referenceThreadAtexitImage(dsoSymbol, __func__); } void APS5_VABI sceKernelSetThreadAtexitCount(get_thread_atexit_count_func_t func) { @@ -66,16 +124,12 @@ void APS5_VABI _sceKernelRtldSetApplicationHeapAPI_nid_postfix(void* api[]) { registerApplicationHeapApi(api); } -int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(std::uint64_t* counter) { - (void)counter; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void* dsoSymbol) { + return releaseThreadAtexitImage(dsoSymbol, "_sceKernelRtldThreadAtexitDecrement"); } -int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(std::uint64_t* counter) { - (void)counter; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void* dsoSymbol) { + return referenceThreadAtexitImage(dsoSymbol, "_sceKernelRtldThreadAtexitIncrement"); } void APS5_VABI _sceKernelSetThreadAtexitCount_nid_postfix(get_thread_atexit_count_func_t callback) { diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 0b27282bb..1b3172e67 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -2,14 +2,26 @@ #include #include #include +#include #include extern "C" { void* APS5_VABI dlopen_nid_postfix(const char*, int); void* APS5_VABI dlsym_nid_postfix(void*, const char*); int APS5_VABI dlclose_nid_postfix(void*); char* APS5_VABI dlerror_nid_postfix(); +int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void*); +int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void*); } static void Require(bool value) { if (!value) std::abort(); } +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} int main(int argc, char** argv) { Require(argc == 2); Require(dlerror_nid_postfix() == nullptr); @@ -61,4 +73,19 @@ int main(int argc, char** argv) { Require(dlclose_nid_postfix(redirected) == 0); std::filesystem::remove(guest); std::filesystem::remove(relinked); + + void* pinned = dlopen_nid_postfix(argv[1], 2); + add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); + Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(dlclose_nid_postfix(pinned) == 0); + Require(add(20, 22) == 42); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(add(40, 2) == 42); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); + Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)); })); + int local = 0; + Require(ThrowsRuntimeError([&] { _sceKernelRtldThreadAtexitIncrement_nid_postfix(&local); })); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..403f72dfb 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -72,6 +72,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [std::_Fiopen](../../core/libs/prx/libc/src/CxxAbiSupport.cpp) (libc) - `_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi` (NID `vYWK2Pz8vGE`; the name comes from the public NID list): the values of the `_Openmode` argument and the meaning of the protection argument are unknown, so it throws when called. It is exported so that a title that imports it (PPSA21564 and PPSA21567 do) loads against the host libc when its own `libc.prx` is left out of the guest conversion. - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw +- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED throws: FreeBSD 9.0 libthr rejects it with EINVAL and 11.0 accepts it, and which one the console follows is unknown From 34ba33faf7d2e47e3da5524d1336bf05ea22d564 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 6 Oct 2026 09:23:44 +0200 Subject: [PATCH 044/203] feat(font): implement sceFontGetFontGlyphsCount and sceFontGetCharGlyphCode Report the face's glyph count and the glyph index of a character through the library's driver; the FreeType driver gains its glyphs_count entry. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/Open.cpp | 41 +++++++++++++++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 10 ----- core/libs/prx/libSceFontFt/src/Driver.cpp | 9 ++++ core/libs/tests/GuestFont.cpp | 23 +++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 3ed2eaa05..e8a045ae0 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -230,6 +230,24 @@ void CopyStyleToCache(FontHandleNative* font) { std::memcpy(&font->cached_style.state, &font->style, sizeof(StyleStateBlock)); } +template +int CallOpenFontObject(FontHandle fontHandle, TCall call) { + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + auto* library = static_cast(font->library); + if (!library || library->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (!library->sys_driver) return SCE_FONT_ERROR_FATAL; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(FontContext(library, font), font->open_info.ctx_entry_index, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_FATAL; + FontObj* obj = FindSubFont(head, font->open_info.sub_font_index); + const int rc = (lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && obj ? call(*library->sys_driver, obj) : SCE_FONT_ERROR_FATAL; + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return rc; +} + } #pragma GCC visibility push(default) @@ -453,6 +471,29 @@ int APS5_VABI sceFontGetLibrary(FontHandle fontHandle, FontLibrary* pLibrary) { return SCE_FONT_OK; } +int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* glyphsCount) { + if (!glyphsCount) return SCE_FONT_ERROR_INVALID_PARAMETER; + *glyphsCount = 0; + const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if (!driver.glyphs_count) return SCE_FONT_ERROR_FATAL; + return driver.glyphs_count(obj, glyphsCount); + }); + if (rc != SCE_FONT_OK) *glyphsCount = 0; + return rc; +} + +int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, std::uint32_t* glyphCode) { + if (!glyphCode) return SCE_FONT_ERROR_INVALID_PARAMETER; + *glyphCode = 0; + const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if (!driver.glyph_index) return SCE_FONT_ERROR_FATAL; + if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + return driver.glyph_index(obj, code, glyphCode); + }); + if (rc != SCE_FONT_OK) *glyphCode = 0; + return rc; +} + int APS5_VABI sceFontBindRenderer(FontHandle fontHandle, FontRenderer renderer) { auto* font = GetNativeFont(fontHandle); std::uint32_t fontLock = 0; diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index e7a004051..8ef042d3c 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -72,16 +72,6 @@ int APS5_VABI sceFontGetAttribute() { return 0; } -int APS5_VABI sceFontGetCharGlyphCode() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontGlyphsCount() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index ad09bd5a2..7f49954e0 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -295,6 +295,14 @@ int APS5_VABI LibraryGetFaceMetric(FontObj* obj, std::uint32_t metricId, std::ui return SCE_FONT_OK; } +int APS5_VABI LibraryGetGlyphsCount(FontObj* obj, std::uint32_t* outCount) { + if (!obj || !outCount) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face || face->num_glyphs < 0) return SCE_FONT_ERROR_FATAL; + *outCount = static_cast(face->num_glyphs); + return SCE_FONT_OK; +} + int APS5_VABI LibraryGetGlyphIndex(FontObj* obj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex) { if (!obj || !outGlyphIndex) return SCE_FONT_ERROR_FATAL; const FT_Face face = FaceOf(obj); @@ -735,6 +743,7 @@ SysDriver MakeDriverTable() { driver.close = &LibraryCloseFont; driver.scale = &LibraryGetFaceScale; driver.metric = &LibraryGetFaceMetric; + driver.glyphs_count = &LibraryGetGlyphsCount; driver.glyph_index = &LibraryGetGlyphIndex; driver.set_char_with_dpi = &LibrarySetCharSizeWithDpi; driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 74ebc5694..d70ac1bdd 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -29,6 +29,8 @@ int APS5_VABI sceFontUnbindRenderer(FontHandle); int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); int APS5_VABI sceFontSetupRenderScalePoint(FontHandle, float, float); int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); +int APS5_VABI sceFontGetFontGlyphsCount(FontHandle, std::uint32_t*); +int APS5_VABI sceFontGetCharGlyphCode(FontHandle, std::uint32_t, std::uint32_t*); int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); @@ -171,6 +173,20 @@ int main() { hDpi = 1; vDpi = 1; Require(sceFontGetResolutionDpi(&unopened, &hDpi, &vDpi) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && hDpi == 0 && vDpi == 0); + std::uint32_t glyphsCount = 0; + Require(sceFontGetFontGlyphsCount(font, &glyphsCount) == SCE_FONT_OK && glyphsCount == 1); + Require(sceFontGetFontGlyphsCount(font, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphsCount = 1; + Require(sceFontGetFontGlyphsCount(nullptr, &glyphsCount) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphsCount == 0); + glyphsCount = 1; + Require(sceFontGetFontGlyphsCount(&unopened, &glyphsCount) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphsCount == 0); + std::uint32_t glyphCode = 1; + Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); + Require(sceFontGetCharGlyphCode(font, 'A', nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(nullptr, 'A', &glyphCode) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphCode == 0); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(&unopened, 'A', &glyphCode) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphCode == 0); Require(sceFontCloseFont(font) == SCE_FONT_OK); FontRenderer renderer = nullptr; @@ -179,6 +195,13 @@ int main() { const std::vector kerningData = KerningFont(); font = nullptr; Require(sceFontOpenFontMemory(library, kerningData.data(), static_cast(kerningData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + Require(sceFontGetFontGlyphsCount(font, &glyphsCount) == SCE_FONT_OK && glyphsCount == 3); + Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_OK && glyphCode == 1); + Require(sceFontGetCharGlyphCode(font, 'V', &glyphCode) == SCE_FONT_OK && glyphCode == 2); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(font, 'B', &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(font, 0, &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_CODE && glyphCode == 0); Require(sceFontSetResolutionDpi(font, 144, 144) == SCE_FONT_OK); Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..233324814 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -46,6 +46,7 @@ Throughout the project, every function at every stage either **does exactly what - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` +- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted From 27cb27dd674e6bfabf525130d4df2a68d7bfbc4f Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 6 Oct 2026 09:34:04 +0200 Subject: [PATCH 045/203] docs(font): cite SharpProspero for the glyph count and glyph code signatures Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 233324814..1687f3c7c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -46,7 +46,7 @@ Throughout the project, every function at every stage either **does exactly what - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` -- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` +- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and matching the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted From 025606ceb454a1163d04ac7d0ae6cc48839e6cbb Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 6 Oct 2026 09:34:56 +0200 Subject: [PATCH 046/203] feat(font): implement sceFontGetFontResolution Report the face's units per EM and the driver's face scale. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/Open.cpp | 17 +++++++++++++++++ core/libs/prx/libSceFont/src/Unimplemented.cpp | 5 ----- core/libs/tests/GuestFont.cpp | 13 +++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index e8a045ae0..ae87bf31b 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -494,6 +494,23 @@ int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, return rc; } +int APS5_VABI sceFontGetFontResolution(FontHandle fontHandle, std::uint32_t* pResolution, float* pScalePixel) { + std::uint16_t unitsPerEm = 0; + float scalePixel = 0.0f; + const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if (!pResolution && !pScalePixel) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!driver.scale) return SCE_FONT_ERROR_FATAL; + return driver.scale(obj, &unitsPerEm, &scalePixel); + }); + if (rc != SCE_FONT_OK) { + unitsPerEm = 0; + scalePixel = 0.0f; + } + if (pResolution) *pResolution = unitsPerEm; + if (pScalePixel) *pScalePixel = scalePixel; + return rc; +} + int APS5_VABI sceFontBindRenderer(FontHandle fontHandle, FontRenderer renderer) { auto* font = GetNativeFont(fontHandle); std::uint32_t fontLock = 0; diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 8ef042d3c..cbd2e3301 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -82,11 +82,6 @@ int APS5_VABI sceFontGetFontMetrics() { return 0; } -int APS5_VABI sceFontGetFontResolution() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontStyleInformation() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index d70ac1bdd..7fd375b21 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -31,6 +31,7 @@ int APS5_VABI sceFontSetupRenderScalePoint(FontHandle, float, float); int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); int APS5_VABI sceFontGetFontGlyphsCount(FontHandle, std::uint32_t*); int APS5_VABI sceFontGetCharGlyphCode(FontHandle, std::uint32_t, std::uint32_t*); +int APS5_VABI sceFontGetFontResolution(FontHandle, std::uint32_t*, float*); int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); @@ -187,6 +188,18 @@ int main() { Require(sceFontGetCharGlyphCode(nullptr, 'A', &glyphCode) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphCode == 0); glyphCode = 1; Require(sceFontGetCharGlyphCode(&unopened, 'A', &glyphCode) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphCode == 0); + std::uint32_t resolution = 0; + float scalePixel = 0.0f; + Require(sceFontGetFontResolution(font, &resolution, &scalePixel) == SCE_FONT_OK && resolution == 1000 && scalePixel == 15.625f); + resolution = 0; + Require(sceFontGetFontResolution(font, &resolution, nullptr) == SCE_FONT_OK && resolution == 1000); + scalePixel = 0.0f; + Require(sceFontGetFontResolution(font, nullptr, &scalePixel) == SCE_FONT_OK && scalePixel == 15.625f); + Require(sceFontGetFontResolution(font, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetFontResolution(nullptr, &resolution, &scalePixel) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && resolution == 0 && scalePixel == 0.0f); + resolution = 1; + scalePixel = 1.0f; + Require(sceFontGetFontResolution(&unopened, &resolution, &scalePixel) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && resolution == 0 && scalePixel == 0.0f); Require(sceFontCloseFont(font) == SCE_FONT_OK); FontRenderer renderer = nullptr; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1687f3c7c..5a98f9e98 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -47,6 +47,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` - [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and matching the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` +- [sceFontGetFontResolution](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title; the outputs are inferred from the names. The resolution is the face's units per EM and the scale pixel the driver's face scale (units per EM / 64 for FreeType), the pixel scale a font has when opened. Either output may be null, as in `sceFontGetResolutionDpi` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted From 0275abd8a89e1fe2643c1164f04024f972ec7518 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 6 Oct 2026 09:48:06 +0200 Subject: [PATCH 047/203] test(agc): skip the 2D derivative cases without non-constant texel offsets The 2D shader samples with computed texel offsets, which the recompiler accepts only with VK_KHR_maintenance8; without it the whole test failed before the 3D and 1D cases ran. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/ImageSampleDerivatives.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp index 5c29b7e6c..34488c71a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp @@ -329,10 +329,12 @@ int main() { if (device->Target().subgroupSize < Threads) { std::printf("EXEC-masked cases skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); } + const bool computedOffsets = device->Target().nonConstantImageOffsets; + if (!computedOffsets) std::puts("2D cases skipped, their computed texel offsets need VK_KHR_maintenance8"); std::vector waves{Threads}; if (device->Target().subgroupSize >= Threads) waves.push_back(Wave64Threads); for (const auto threads : waves) { - Run(*device, Code2D, threads, 9u, Height, 1u, Names2D, Masked2D, Expected2D); + if (computedOffsets) Run(*device, Code2D, threads, 9u, Height, 1u, Names2D, Masked2D, Expected2D); Run(*device, Code3D, threads, 10u, Height, Depth, Names3D, Masked3D, Expected3D); Run(*device, Code1D, threads, 8u, 1u, 1u, Names1D, Masked1D, Expected1D); } From e302d5507f30ef569fc80211bf6bcc47e062e212 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 6 Oct 2026 10:16:21 +0200 Subject: [PATCH 048/203] fix(shader): serialize locked LDS updates with a wave-uniform loop exit Each lane left the lock loop as soon as it acquired the lock, so the driver's compiler could sink the critical section and the release past the loop exit; on AMD the holder then waited outside the loop for lanes spinning on its lock, and the GPU hung until a TDR. One elected lane now takes the lock per iteration and the loop exits once a ballot shows no lane left. Co-Authored-By: Claude Opus 5.5 --- .../SpirvBackend/src/SpirvAnalysis.cpp | 1 + .../SpirvMemory/SpirvMemoryInstructions.cpp | 56 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 82fdd645b..927c8ca8c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -363,6 +363,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { requirements.functionLds = true; } else if (sharedAccess == SharedAccess::Atomic && inst->Type() == IrType::U64 && kind == ResourceKind::Lds) { requirements.ldsLock = true; + requirements.subgroupBallot = true; } if (sharedAccess == SharedAccess::Append || sharedAccess == SharedAccess::Consume) { requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 3657db5d6..a636d6699 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1214,35 +1214,73 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, TUpdate&& update) { const auto lock = EmitLdsLockPointer(state); + const auto entry = state.currentLabel; const auto header = state.module.AllocateId(); const auto body = state.module.AllocateId(); + const auto pending = state.module.AllocateId(); + const auto spinHeader = state.module.AllocateId(); + const auto spinBody = state.module.AllocateId(); + const auto spinCont = state.module.AllocateId(); const auto critical = state.module.AllocateId(); - const auto after = state.module.AllocateId(); + const auto servedMerge = state.module.AllocateId(); + const auto pendingMerge = state.module.AllocateId(); const auto cont = state.module.AllocateId(); const auto merge = state.module.AllocateId(); + const auto done = state.module.AllocateId(); + const auto result = state.module.AllocateId(); + const auto doneNext = state.module.AllocateId(); + const auto resultNext = state.module.AllocateId(); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, TypeBool(state), done, ConstantBool(state, false), entry, doneNext, cont); + state.module.AddFunction(spv::OpPhi, TypeU64(state), result, ConstantU64(state, 0u), entry, resultNext, cont); state.module.AddFunction(spv::OpLoopMerge, merge, cont, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranch, body); EmitLabel(state, body); + state.module.AddFunction(spv::OpSelectionMerge, pendingMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, done, pendingMerge, pending); + EmitLabel(state, pending); + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + state.module.AddFunction(spv::OpSelectionMerge, servedMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, elected, spinHeader, servedMerge); + EmitLabel(state, spinHeader); + state.module.AddFunction(spv::OpLoopMerge, critical, spinCont, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranch, spinBody); + EmitLabel(state, spinBody); const auto previous = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsWorkgroupMemoryMask), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, 1u), ConstantU32(state, 0u)); const auto acquired = Binary(state, spv::OpIEqual, TypeBool(state), previous, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpSelectionMerge, after, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, acquired, critical, after); + state.module.AddFunction(spv::OpBranchConditional, acquired, critical, spinCont); + EmitLabel(state, spinCont); + state.module.AddFunction(spv::OpBranch, spinHeader); EmitLabel(state, critical); const auto old = update(); state.module.AddFunction(spv::OpAtomicStore, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask), ConstantU32(state, 0u)); const auto criticalExit = state.currentLabel; - state.module.AddFunction(spv::OpBranch, after); - EmitLabel(state, after); - const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpPhi, TypeU64(state), result, old, criticalExit, ConstantU64(state, 0u), body); - state.module.AddFunction(spv::OpBranchConditional, acquired, merge, cont); + state.module.AddFunction(spv::OpBranch, servedMerge); + EmitLabel(state, servedMerge); + const auto served = state.module.AllocateId(); + const auto servedResult = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeBool(state), served, ConstantBool(state, true), criticalExit, ConstantBool(state, false), pending); + state.module.AddFunction(spv::OpPhi, TypeU64(state), servedResult, old, criticalExit, result, pending); + state.module.AddFunction(spv::OpBranch, pendingMerge); + EmitLabel(state, pendingMerge); + state.module.AddFunction(spv::OpPhi, TypeBool(state), doneNext, served, servedMerge, done, body); + state.module.AddFunction(spv::OpPhi, TypeU64(state), resultNext, servedResult, servedMerge, result, body); + const auto ballot = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), Unary(state, spv::OpLogicalNot, TypeBool(state), doneNext)); + std::uint32_t remaining = ConstantU32(state, 0u); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto word = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), word, ballot, component); + remaining = Binary(state, spv::OpBitwiseOr, TypeU32(state), remaining, word); + } + state.module.AddFunction(spv::OpBranchConditional, Binary(state, spv::OpINotEqual, TypeBool(state), remaining, ConstantU32(state, 0u)), cont, merge); EmitLabel(state, cont); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - return result; + return resultNext; } template From 897d538649dbab419c828423b19233cc4e883868 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 08:26:50 +0000 Subject: [PATCH 049/203] refactor(relinker): use the shared ELF constants The relinker redefined 61 ELF constants 93 times across seven files, 38 of them already in elfpatcher's ElfConstants.hpp. Add the missing dynamic tags, relocation types and symbol constants to that header and remove the local copies; every value is unchanged. strict_nid_filter_tests compiles these sources and now gets the elfpatcher include directory. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MBXJqzgohLeVAeHsn4Jpm7 --- core/relinker/CMakeLists.txt | 1 + .../elfpatcher/general/ElfConstants.hpp | 47 +++++++++++++++++++ .../output/SysVDynamicSectionBuilder.hpp | 19 -------- .../relinker/pipeline/RelinkerPipeline.hpp | 30 ------------ .../UnusedNidFilter/EntryPointCollector.cpp | 24 ++-------- .../RelativeRelocationIndex.cpp | 16 ++----- .../src/analysis/ValidationPolicy.cpp | 18 ++----- .../src/output/SysVDynamicSectionBuilder.cpp | 3 ++ .../relinker/src/parsing/ElfReader.cpp | 5 +- .../src/pipeline/RelinkerPipeline.cpp | 7 ++- 10 files changed, 66 insertions(+), 104 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 006738d94..13be7368a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -265,6 +265,7 @@ if(BUILD_TESTING) ) target_include_directories(strict_nid_filter_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include ) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index c6f63f41d..c67ededd1 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -87,12 +87,59 @@ inline constexpr std::int64_t DT_STRSZ = 10; inline constexpr std::int64_t DT_SYMENT = 11; inline constexpr std::int64_t DT_PLTREL = 20; inline constexpr std::int64_t DT_JMPREL = 23; +inline constexpr std::int64_t DT_INIT = 12; +inline constexpr std::int64_t DT_FINI = 13; +inline constexpr std::int64_t DT_INIT_ARRAY = 25; +inline constexpr std::int64_t DT_FINI_ARRAY = 26; +inline constexpr std::int64_t DT_INIT_ARRAYSZ = 27; +inline constexpr std::int64_t DT_FINI_ARRAYSZ = 28; inline constexpr std::int64_t DT_RUNPATH = 29; inline constexpr std::int64_t DT_FLAGS = 30; inline constexpr std::uint64_t DF_BIND_NOW = 0x8; +inline constexpr std::int64_t DT_OS_INIT = 0x6000000c; +inline constexpr std::int64_t DT_OS_FINI = 0x6000000d; +inline constexpr std::int64_t DT_OS_INIT_ARRAY = 0x60000019; +inline constexpr std::int64_t DT_OS_FINI_ARRAY = 0x6000001a; +inline constexpr std::int64_t DT_OS_INIT_ARRAYSZ = 0x6000001b; +inline constexpr std::int64_t DT_OS_FINI_ARRAYSZ = 0x6000001c; +inline constexpr std::int64_t DT_OS_PLTGOT = 0x61000027; +inline constexpr std::int64_t DT_OS_JMPREL = 0x61000029; +inline constexpr std::int64_t DT_OS_PLTREL = 0x6100002b; +inline constexpr std::int64_t DT_OS_PLTRELSZ = 0x6100002d; +inline constexpr std::int64_t DT_OS_RELA = 0x6100002f; +inline constexpr std::int64_t DT_OS_RELASZ = 0x61000031; +inline constexpr std::int64_t DT_OS_RELAENT = 0x61000033; +inline constexpr std::int64_t DT_OS_STRTAB = 0x61000035; +inline constexpr std::int64_t DT_OS_STRSZ = 0x61000037; +inline constexpr std::int64_t DT_OS_SYMTAB = 0x61000039; +inline constexpr std::int64_t DT_OS_SYMENT = 0x6100003b; +inline constexpr std::int64_t DT_OS_SYMTABSZ = 0x6100003f; + +inline constexpr std::uint32_t R_X86_64_NONE = 0; +inline constexpr std::uint32_t R_X86_64_64 = 1; +inline constexpr std::uint32_t R_X86_64_PC32 = 2; +inline constexpr std::uint32_t R_X86_64_GOT32 = 3; +inline constexpr std::uint32_t R_X86_64_PLT32 = 4; +inline constexpr std::uint32_t R_X86_64_COPY = 5; +inline constexpr std::uint32_t R_X86_64_GLOB_DAT = 6; +inline constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; +inline constexpr std::uint32_t R_X86_64_RELATIVE = 8; +inline constexpr std::uint32_t R_X86_64_GOTPCREL = 9; +inline constexpr std::uint32_t R_X86_64_32 = 10; +inline constexpr std::uint32_t R_X86_64_32S = 11; +inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; +inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; + +inline constexpr std::uint8_t STB_GLOBAL = 1; +inline constexpr std::uint8_t STB_WEAK = 2; +inline constexpr std::uint8_t STT_FUNC = 2; +inline constexpr std::uint8_t STV_DEFAULT = 0; +inline constexpr std::uint16_t SHN_UNDEF = 0; + inline constexpr std::uint32_t SHT_NULL = 0; inline constexpr std::uint32_t SHT_PROGBITS = 1; +inline constexpr std::uint32_t SHT_SYMTAB = 2; inline constexpr std::uint32_t SHT_STRTAB = 3; inline constexpr std::uint32_t SHT_DYNAMIC = 6; inline constexpr std::uint32_t SHT_DYNSYM = 11; diff --git a/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp b/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp index 7816d126c..c75274640 100644 --- a/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp +++ b/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp @@ -15,25 +15,6 @@ public: ) override; private: - static constexpr std::uint32_t STB_GLOBAL = 1; - static constexpr std::uint32_t STT_FUNC = 2; - static constexpr std::uint32_t STV_DEFAULT = 0; - static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; - static constexpr std::uint32_t R_X86_64_GLOB_DAT = 6; - - static constexpr std::int64_t DT_NEEDED = 1; - static constexpr std::int64_t DT_SYMTAB = 6; - static constexpr std::int64_t DT_STRTAB = 5; - static constexpr std::int64_t DT_STRSZ = 10; - static constexpr std::int64_t DT_RELA = 7; - static constexpr std::int64_t DT_RELASZ = 8; - static constexpr std::int64_t DT_RELAENT = 9; - static constexpr std::int64_t DT_JMPREL = 23; - static constexpr std::int64_t DT_PLTRELSZ = 2; - static constexpr std::int64_t DT_PLTREL = 20; - static constexpr std::int64_t DT_SYMENT = 11; - static constexpr std::int64_t DT_NULL = 0; - void _appendU64(std::vector& buf, std::uint64_t v) const; void _appendI64(std::vector& buf, std::int64_t v) const; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; diff --git a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp index 67be9ac4b..78554168b 100644 --- a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp @@ -27,36 +27,6 @@ private: std::shared_ptr _unusedNidFilter; std::uint32_t unusedFilterLevel; - static constexpr std::uint32_t PT_LOAD = 1; - static constexpr std::uint32_t PT_DYNAMIC = 2; - static constexpr std::uint32_t PF_X = 0x1; - - static constexpr std::int64_t DT_NEEDED = 0x00000001; - static constexpr std::int64_t DT_PLTRELSZ = 0x00000002; - static constexpr std::int64_t DT_PLTGOT = 0x00000003; - static constexpr std::int64_t DT_STRTAB = 0x00000005; - static constexpr std::int64_t DT_SYMTAB = 0x00000006; - static constexpr std::int64_t DT_RELA = 0x00000007; - static constexpr std::int64_t DT_RELASZ = 0x00000008; - static constexpr std::int64_t DT_RELAENT = 0x00000009; - static constexpr std::int64_t DT_STRSZ = 0x0000000a; - static constexpr std::int64_t DT_SYMENT = 0x0000000b; - static constexpr std::int64_t DT_PLTREL = 0x00000014; - static constexpr std::int64_t DT_JMPREL = 0x00000017; - - static constexpr std::int64_t DT_OS_PLTGOT = 0x61000027; - static constexpr std::int64_t DT_OS_PLTREL = 0x6100002b; - static constexpr std::int64_t DT_OS_PLTRELSZ = 0x6100002d; - static constexpr std::int64_t DT_OS_STRTAB = 0x61000035; - static constexpr std::int64_t DT_OS_STRSZ = 0x61000037; - static constexpr std::int64_t DT_OS_SYMTAB = 0x61000039; - static constexpr std::int64_t DT_OS_SYMENT = 0x6100003b; - static constexpr std::int64_t DT_OS_SYMTABSZ = 0x6100003f; - static constexpr std::int64_t DT_OS_RELA = 0x6100002f; - static constexpr std::int64_t DT_OS_RELASZ = 0x61000031; - static constexpr std::int64_t DT_OS_RELAENT = 0x61000033; - static constexpr std::int64_t DT_OS_JMPREL = 0x61000029; - static std::string _relocationTypeName(std::uint32_t type); }; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index c2225509f..d24bcaf3d 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -1,8 +1,11 @@ #include +#include #include namespace Relinker::UnusedNidFilter { +using namespace Elfpatcher; + namespace { std::uint16_t read16(const std::vector& b, std::size_t off) { @@ -65,7 +68,6 @@ public: std::uint32_t type = read32(elfBytes, phPos); - constexpr std::uint32_t PT_DYNAMIC = 2; if (type != PT_DYNAMIC) continue; std::uint64_t segOff = read64(elfBytes, phPos + 8); @@ -73,20 +75,6 @@ public: if (segOff + segSz > elfBytes.size()) throw RelinkerException("PT_DYNAMIC segment out of bounds"); - constexpr std::int64_t DT_NULL = 0; - constexpr std::int64_t DT_INIT = 12; - constexpr std::int64_t DT_FINI = 13; - constexpr std::int64_t DT_INIT_ARRAY = 25; - constexpr std::int64_t DT_INIT_ARRAYSZ = 27; - constexpr std::int64_t DT_FINI_ARRAY = 26; - constexpr std::int64_t DT_FINI_ARRAYSZ = 28; - constexpr std::int64_t DT_OS_INIT_ARRAY = 0x60000019; - constexpr std::int64_t DT_OS_INIT_ARRAYSZ = 0x6000001b; - constexpr std::int64_t DT_OS_FINI_ARRAY = 0x6000001a; - constexpr std::int64_t DT_OS_FINI_ARRAYSZ = 0x6000001c; - constexpr std::int64_t DT_OS_INIT = 0x6000000c; - constexpr std::int64_t DT_OS_FINI = 0x6000000d; - VirtualAddress initArrayVa = 0; std::uint64_t initArraySz = 0; VirtualAddress finiArrayVa = 0; @@ -142,8 +130,6 @@ public: if (shPos + 64 > elfBytes.size()) break; std::uint32_t shType = read32(elfBytes, shPos + 4); - constexpr std::uint32_t SHT_DYNSYM = 11; - constexpr std::uint32_t SHT_SYMTAB = 2; if (shType != SHT_DYNSYM && shType != SHT_SYMTAB) continue; std::uint64_t symOff = read64(elfBytes, shPos + 24); @@ -172,10 +158,6 @@ public: std::uint8_t stType = info & 0xF; std::uint16_t shndx = read16(elfBytes, sPos + 6); std::uint64_t symVal = read64(elfBytes, sPos + 8); - constexpr std::uint8_t STB_GLOBAL = 1; - constexpr std::uint8_t STB_WEAK = 2; - constexpr std::uint8_t STT_FUNC = 2; - constexpr std::uint16_t SHN_UNDEF = 0; if ((stBind == STB_GLOBAL || stBind == STB_WEAK) && stType == STT_FUNC && shndx != SHN_UNDEF) { addIfInText(symVal); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 9af486615..fd508b987 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -1,9 +1,12 @@ #include +#include #include #include namespace Relinker::UnusedNidFilter { +using namespace Elfpatcher; + namespace { std::uint16_t read16(const std::vector& b, std::size_t off) { @@ -38,7 +41,6 @@ std::uint64_t vaddrToFileOffset(const std::vector& loads, VirtualAd throw RelinkerException("Cannot translate virtual address to file offset", va); } -constexpr std::uint32_t R_X86_64_RELATIVE = 8; constexpr std::size_t RelaEntSize = 24; void collectRelativeEntries( @@ -105,7 +107,6 @@ std::unique_ptr BuildRelativeRelocationIndex( std::size_t phPos = static_cast(phOff) + i * phEntSize; if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); std::uint32_t type = read32(elfBytes, phPos); - constexpr std::uint32_t PT_LOAD = 1; if (type != PT_LOAD) continue; LoadSegment seg; seg.fileOffset = read64(elfBytes, phPos + 8); @@ -121,23 +122,12 @@ std::unique_ptr BuildRelativeRelocationIndex( if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); std::uint32_t type = read32(elfBytes, phPos); - constexpr std::uint32_t PT_DYNAMIC = 2; if (type != PT_DYNAMIC) continue; std::uint64_t segOff = read64(elfBytes, phPos + 8); std::uint64_t segSz = read64(elfBytes, phPos + 32); if (segOff + segSz > elfBytes.size()) throw RelinkerException("PT_DYNAMIC segment out of bounds"); - constexpr std::int64_t DT_NULL = 0; - constexpr std::int64_t DT_RELA = 7; - constexpr std::int64_t DT_RELASZ = 8; - constexpr std::int64_t DT_JMPREL = 0x17; - constexpr std::int64_t DT_PLTRELSZ = 2; - constexpr std::int64_t DT_OS_RELA = 0x6100002f; - constexpr std::int64_t DT_OS_RELASZ = 0x61000031; - constexpr std::int64_t DT_OS_JMPREL = 0x61000029; - constexpr std::int64_t DT_OS_PLTRELSZ = 0x6100002d; - VirtualAddress relaVa = 0; std::uint64_t relaSz = 0; VirtualAddress jmprelVa = 0; diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 4a0a6a7cf..2f2f4f91b 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -1,8 +1,11 @@ #include "relinker/analysis//ValidationPolicy.hpp" +#include #include namespace Relinker { +using namespace Elfpatcher; + static constexpr std::uint8_t SYSCALL_BYTE0 = 0x0F; static constexpr std::uint8_t SYSCALL_BYTE1 = 0x05; static constexpr std::uint8_t INT80_BYTE0 = 0xCD; @@ -12,21 +15,6 @@ static constexpr std::uint8_t SYSENTER_BYTE1 = 0x34; static constexpr std::uint8_t SYSRET_BYTE0 = 0x0F; static constexpr std::uint8_t SYSRET_BYTE1 = 0x07; -static constexpr std::uint32_t R_X86_64_NONE = 0; -static constexpr std::uint32_t R_X86_64_64 = 1; -static constexpr std::uint32_t R_X86_64_PC32 = 2; -static constexpr std::uint32_t R_X86_64_GOT32 = 3; -static constexpr std::uint32_t R_X86_64_PLT32 = 4; -static constexpr std::uint32_t R_X86_64_COPY = 5; -static constexpr std::uint32_t R_X86_64_GLOB_DAT = 6; -static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; -static constexpr std::uint32_t R_X86_64_RELATIVE = 8; -static constexpr std::uint32_t R_X86_64_GOTPCREL = 9; -static constexpr std::uint32_t R_X86_64_32 = 10; -static constexpr std::uint32_t R_X86_64_32S = 11; -static constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; -static constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; - void ValidationPolicy::_initializeSupportedRelocationTypes() { _supportedRelocationTypes = { R_X86_64_NONE, diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 3e29923b9..132d49e72 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -1,8 +1,11 @@ #include +#include #include namespace Relinker { +using namespace Elfpatcher; + void SysVDynamicSectionBuilder::_appendU64(std::vector& buf, std::uint64_t v) const { std::size_t pos = buf.size(); buf.resize(pos + 8); diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index ba8992b17..779dee5a1 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -1,9 +1,12 @@ #include +#include #include #include namespace Relinker { +using namespace Elfpatcher; + namespace { bool _rangeFits(const std::uint64_t offset, const std::uint64_t length, const std::size_t size) { @@ -201,8 +204,6 @@ FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const const VirtualAddress segVAddr = _readU64At(offset + 0x10); const ByteCount segFileSize = _readU64At(offset + 0x20); - static constexpr std::uint32_t PT_LOAD = 1; - if (type == PT_LOAD && address >= segVAddr && address < segVAddr + segFileSize) { return segOffset + (address - segVAddr); } diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2438ce6b3..9e301e7eb 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -7,6 +8,8 @@ namespace Relinker { +using namespace Elfpatcher; + RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel) : _elfReader(std::move(elfReader)) , _syscallScanner(std::move(syscallScanner)) @@ -218,8 +221,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf _validationPolicy->ValidateSyscallAbsence(); - static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; - const std::size_t originalNidCount = nidRefs.size(); const auto originalNidRefs = nidRefs; std::cout << "NID input: " << originalNidCount << " references\n"; @@ -273,8 +274,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } auto dynSection = _dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount); - static constexpr std::uint32_t R_X86_64_RELATIVE = 8; - auto appendRela = [&](std::vector& buf, std::uint64_t offset, std::uint64_t info, std::int64_t addend) { std::size_t pos = buf.size(); buf.resize(pos + 24); From ef10a36b8a43ea60a48ed12dd894cd6f37505f14 Mon Sep 17 00:00:00 2001 From: Merijn Date: Tue, 6 Oct 2026 10:41:54 +0200 Subject: [PATCH 050/203] fix(agc-driver): accept per-engine primitive discard in PA_SC_MODE_CNTL_1 Bit 17 of PA_SC_MODE_CNTL_1 (MULTI_SHADER_ENGINE_PRIM_DISCARD_ENABLE) lets each shader engine drop primitives outside its screen region, a work distribution setting with no visible effect. Draws with it set were skipped as unsupported. PPSA12544 sets it (0x06020000, with the already accepted EOV bits 25 and 26) on the draws that produce its presented image, so every frame it presented was black. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 02a893d26..cab2912f0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -67,7 +67,7 @@ constexpr std::uint32_t DepthControlMask = ~0x007007f0u; constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u); constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t ScanModeMask = ~2u; -constexpr std::uint32_t ScanControlMask = ~0x06003fffu; +constexpr std::uint32_t ScanControlMask = ~0x06023fffu; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; // Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space. constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x01000000u | 0x0c000000u); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d7f881661..9be7d4dce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -108,6 +108,13 @@ void stateTests() { queue.context[0x103] = 5; Require(AgcDriver::Graphics::DrawRejection(queue, true).find("all ones") != std::string::npos, "a restart index other than all ones was accepted"); queue = makeState(); + queue.context[0x293] = 0x06020000u; + (void)AgcDriver::Graphics::DecodeState(queue); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") == std::string::npos, "per-engine primitive discard was rejected"); + queue.context[0x293] = 0x06030000u; + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") != std::string::npos, "per-sample shading was accepted"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "sample iteration"); + queue = makeState(); queue.userConfig.erase(0x24b); queue.context[0x2a5] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "user-config bank at DWORD 0x24b"); From d1d98a82614fe0482565b4a86eb49cc6e623301b Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 23:48:18 +0200 Subject: [PATCH 051/203] fix(savedata): accept a repeated sceSaveDataInitialize3 A second sceSaveDataInitialize3 returned SAVE_DATA_ERROR_ALREADY_INITIALIZED. PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin raises Unity.SaveData.PS5.Core.SaveDataException on any error, so the console must accept the second call. Initializations are now counted; sceSaveDataTerminate ends the session at the last one and only then checks for mounted slots. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 19 ++++++++----------- core/libs/tests/SaveDataMemoryMetadata.cpp | 2 ++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index f0b798f5d..aba825cf2 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -24,7 +24,7 @@ static constexpr char SAVE_DIR[] = "_sd"; static std::atomic g_transaction_counter{1}; -static bool g_initialized = false; +static int g_initializations = 0; static std::string save_root() { return std::string(SAVE_DIR); @@ -351,7 +351,7 @@ static int getSaveDataMemory2(SaveDataMemoryGet2* get_param) { if (get_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); @@ -393,10 +393,7 @@ int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { int APS5_VABI sceSaveDataInitialize3(const void* init) { (void)init; - if (g_initialized) { - return SAVE_DATA_ERROR_ALREADY_INITIALIZED; - } - g_initialized = true; + ++g_initializations; return SAVE_DATA_OK; } @@ -536,7 +533,7 @@ static int setSaveDataMemory2(const SaveDataMemorySet2* set_param) { if (set_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); @@ -594,7 +591,7 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (setup_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } if (setup_param->memory_size == 0 || setup_param->memory_size > MEM_MAX_SIZE) { @@ -654,13 +651,13 @@ int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { } int APS5_VABI sceSaveDataTerminate(void) { - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } - if (any_slot_used()) { + if (g_initializations == 1 && any_slot_used()) { return SAVE_DATA_ERROR_BUSY; } - g_initialized = false; + --g_initializations; return SAVE_DATA_OK; } diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index e3b960ee9..5c75de021 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -34,6 +34,8 @@ int main() { std::filesystem::create_directories(path.parent_path()); { std::ofstream file("_sd_mem/u7531/slot0.bin"); file << "save"; Require(static_cast(file)); } Require(sceSaveDataInitialize3(nullptr) == 0); + Require(sceSaveDataInitialize3(nullptr) == 0); + Require(sceSaveDataTerminate() == 0); SaveDataParam param{}; SaveDataMemorySet2 set{}; set.user_id = 7531; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..ec5128dd7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -118,6 +118,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw +- [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. From 498b53bbf5b0c14bb550669618dabb782bd814e0 Mon Sep 17 00:00:00 2001 From: Mehran Date: Tue, 6 Oct 2026 11:47:31 +0300 Subject: [PATCH 052/203] fix(agc): release pooled buffers and image mirrors before the device --- .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 3 +++ .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 13 +++++++++++++ 3 files changed, 17 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5cadb337f..7ba5d2a3d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -510,8 +510,11 @@ struct VulkanDevice::State { } // Every ShaderResources (kept by the recorder or the resource cache) is gone now, so the // sets and samplers they borrowed can go. + copiedWriters->clear(); resourceCache.Clear(); Graphics::ClearCachedTextures(device); + Graphics::ClearImageMirrors(device); + patternBuffers.clear(); descriptorCache.reset(); emptyBuffer.reset(); samplerCache.reset(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index baf70ca07..0374fc933 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -148,6 +148,7 @@ struct MirrorStats { std::uint64_t heapRefills = 0; }; MirrorStats MirrorCounters(); +void ClearImageMirrors(VkDevice device); struct AddressCopy { std::uint64_t begin; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index bb16630cc..1a1c8941e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1002,6 +1002,19 @@ MirrorStats MirrorCounters() { return stats; } +void ClearImageMirrors(VkDevice device) { + auto& state = Mirrors(); + std::map> entries; + { + std::lock_guard lock(state.mutex); + if (state.device != device) return; + entries.swap(state.entries); + state.failed.clear(); + state.heapBytes = 0; + state.device = VK_NULL_HANDLE; + } +} + ImportProbe ProbeImportWriteProtection(const Context& context) { ImportProbe probe; #ifdef _WIN32 From e6d0c9c9b0e8329dfa6aeef9ffb0dacc06e4f4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:48:23 +0200 Subject: [PATCH 053/203] feat(libSceHttp): implement sceHttpUriSweepPath Co-Authored-By: Claude --- core/libs/prx/libSceHttp/src/Uri.cpp | 42 ++++++++++++++++++++++++++++ core/libs/tests/GuestHttp.cpp | 23 +++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 4dc4dd3b3..b9a300ea4 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -254,4 +255,45 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* return 0; } +int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { + if (srcSize == 0) return 0; + if (!dst || !src) return ERROR_INVALID_VALUE; + + const size_t length = srcSize - 1; + if (src[0] != '/') { + std::memcpy(dst, src, length); + dst[length] = '\0'; + return 0; + } + + dst[0] = '/'; + dst[1] = '\0'; + size_t end = 0; + size_t pos = 1; + while (pos < length) { + if (src[pos] == '.' && src[pos + 1] == '/') { + pos += 2; + continue; + } + if (src[pos] == '.' && src[pos + 1] == '.' && src[pos + 2] == '/') { + if (end != 0) { + dst[end] = '\0'; + end = static_cast(std::strrchr(dst, '/') - dst); + dst[end + 1] = '\0'; + } + pos += 3; + continue; + } + size_t count = length - pos; + if (const char* slash = std::strchr(src + pos, '/')) { + count = std::min(static_cast(slash + 1 - (src + pos)), count); + } + std::memcpy(dst + end + 1, src + pos, count); + dst[end + 1 + count] = '\0'; + end += count; + pos += count; + } + return 0; +} + } diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index ae625c507..75d86a904 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -11,6 +11,7 @@ int APS5_VABI sceHttpSetInflateGZIPEnabled(int, int); int APS5_VABI sceHttpUriBuild(char*, std::size_t*, std::size_t, const SceHttpUriElement*, std::uint32_t); int APS5_VABI sceHttpUriEscape(char*, std::size_t*, std::size_t, const char*); int APS5_VABI sceHttpUriUnescape(char*, std::size_t*, std::size_t, const char*); +int APS5_VABI sceHttpUriSweepPath(char*, const char*, std::size_t); int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); int APS5_VABI sceHttpReadData(int, void*, std::size_t); @@ -194,4 +195,26 @@ int main() { Require(parse(response, 15) == parseInvalidResponse); Require(parse(response, 16) == parseInvalidResponse); Require(phrase == nullptr && phraseLength == 0); + auto sweep = [](const char* src, const char* expected) { + char swept[128]; + std::memset(swept, 'x', sizeof(swept)); + return sceHttpUriSweepPath(swept, src, std::strlen(src) + 1) == 0 && Equal(swept, expected); + }; + Require(sceHttpUriSweepPath(nullptr, nullptr, 0) == 0); + Require(sceHttpUriSweepPath(nullptr, "/foo", 5) == invalidValue); + char sweptPath[16]; + Require(sceHttpUriSweepPath(sweptPath, nullptr, 5) == invalidValue); + Require(sweep("foo/../bar", "foo/../bar")); + Require(sweep("/foo/../bar", "/bar")); + Require(sweep("/foo/./bar", "/foo/bar")); + Require(sweep("/foo/.", "/foo/.")); + Require(sweep("/foo/..", "/foo/..")); + Require(sweep("/foo/bar/../foo/././../../../test/index.html", "/test/index.html")); + Require(sweep("/", "/")); + Require(sweep("", "")); + Require(sweep("/a/b/c/../../d/", "/a/d/")); + Require(sweep("/../a", "/a")); + Require(sweep("/a/..b/.c", "/a/..b/.c")); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 6) == 0 && Equal(sweptPath, "/a/b/")); + Require(sceHttpUriSweepPath(sweptPath, "/ab/c", 3) == 0 && Equal(sweptPath, "/a")); } From 0207e28f882e4ba136963c3bf9f7ae43ef10955e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:55:33 +0200 Subject: [PATCH 054/203] feat(libSceHttp): implement sceHttpUriMerge Co-Authored-By: Claude --- core/libs/prx/libSceHttp/src/Uri.cpp | 53 ++++++++++++++++++++++++++-- core/libs/tests/GuestHttp.cpp | 48 +++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 4dc4dd3b3..68c4e3c71 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -19,6 +20,7 @@ constexpr uint32_t URI_BUILD_WITH_USERNAME = 0x10; constexpr uint32_t URI_BUILD_WITH_PASSWORD = 0x20; constexpr uint32_t URI_BUILD_WITH_QUERY = 0x40; constexpr uint32_t URI_BUILD_WITH_FRAGMENT = 0x80; +constexpr size_t URI_MAX_LENGTH = 0x3FFF; struct UriParts { bool opaque = false; @@ -140,6 +142,13 @@ int parseUri(std::string_view uri, UriParts& parts) { return 0; } +size_t poolSize(const UriParts& parts) { + const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; + size_t size = 0; + for (const std::string_view field : fields) size += field.size() + 1; + return size; +} + int copyOut(const std::string& value, char* out, size_t* require, size_t prepare) { if (require) *require = value.size() + 1; if (!out) return 0; @@ -218,6 +227,47 @@ int APS5_VABI sceHttpUriUnescape(char* out, size_t* require, size_t prepare, con return copyOut(decoded, out, require, prepare); } +int APS5_VABI sceHttpUriMerge(char* merged_url, const char* url, const char* relative_uri, size_t* require, size_t prepare, uint32_t option) { + if (option != 0 || !url || !relative_uri) return ERROR_INVALID_VALUE; + + UriParts base; + if (const int result = parseUri(url, base); result != 0) return result; + UriParts relative; + if (const int result = parseUri(relative_uri, relative); result != 0) return result; + + const size_t urlLength = strnlen(url, URI_MAX_LENGTH); + const size_t relativeLength = strnlen(relative_uri, URI_MAX_LENGTH); + const size_t size = poolSize(base) + 2 + (urlLength + relativeLength) * 2; + if (require) *require = size; + if (!merged_url) return 0; + if (prepare < size) return ERROR_OUT_OF_MEMORY; + + if (!relative.opaque) { + std::strncpy(merged_url, relative_uri, size); + if (require) *require = relativeLength + 1; + return 0; + } + + std::string path = base.path; + const size_t slash = path.rfind('/'); + if (slash == std::string::npos) path.push_back('/'); + else path.erase(slash + 1); + if (relative_uri[0] == '/') path.clear(); + path.append(relative_uri, std::min(relativeLength, URI_MAX_LENGTH - std::min(path.size(), URI_MAX_LENGTH))); + + std::string scheme(base.scheme), username(base.username), password(base.password), hostname(base.hostname); + SceHttpUriElement element; + element.opaque = base.opaque; + element.scheme = scheme.data(); + element.username = username.data(); + element.password = password.data(); + element.hostname = hostname.data(); + element.path = path.data(); + element.port = base.port; + return sceHttpUriBuild(merged_url, nullptr, prepare - (urlLength + relativeLength + 1), &element, + URI_BUILD_WITH_SCHEME | URI_BUILD_WITH_HOSTNAME | URI_BUILD_WITH_PORT | URI_BUILD_WITH_PATH | URI_BUILD_WITH_USERNAME | URI_BUILD_WITH_PASSWORD); +} + int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* pool, size_t* require, size_t prepare) { if (!src_url) return ERROR_INVALID_URL; const bool write = out && pool; @@ -227,8 +277,7 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* if (const int result = parseUri(src_url, parts); result != 0) return result; const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; - size_t size = 0; - for (const std::string_view field : fields) size += field.size() + 1; + const size_t size = poolSize(parts); if (require) *require = size; if (!write) return 0; if (prepare < size) return ERROR_OUT_OF_MEMORY; diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index ae625c507..4dbcc057f 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -7,6 +7,7 @@ extern "C" { int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); +int APS5_VABI sceHttpUriMerge(char*, const char*, const char*, std::size_t*, std::size_t, std::uint32_t); int APS5_VABI sceHttpSetInflateGZIPEnabled(int, int); int APS5_VABI sceHttpUriBuild(char*, std::size_t*, std::size_t, const SceHttpUriElement*, std::uint32_t); int APS5_VABI sceHttpUriEscape(char*, std::size_t*, std::size_t, const char*); @@ -125,6 +126,53 @@ int main() { Require(sceHttpUriUnescape(decodedBytes, &required, sizeof(decodedBytes), encodedBytes) == 0); Require(required == sizeof(bytes) && std::memcmp(bytes, decodedBytes, sizeof(bytes)) == 0); + const char* base = "http://foo.com/foo/index.html"; + const std::size_t baseMergeSize = 5 + 1 + 1 + 8 + 16 + 1 + 1 + 2; + char merged[512]; + auto merge = [&](const char* mergeBase, const char* relative, const char* expected) { + std::memset(merged, 'Z', sizeof(merged)); + return sceHttpUriMerge(merged, mergeBase, relative, &required, sizeof(merged), 0) == 0 && Equal(merged, expected); + }; + Require(merge(base, "./default.html", "http://foo.com/foo/./default.html")); + Require(required == baseMergeSize + 2 * (29 + 14)); + Require(merge(base, "../sibling.html", "http://foo.com/foo/../sibling.html")); + Require(merge(base, "", "http://foo.com/foo/")); + Require(merge(base, "/root.html", "http://foo.com/root.html")); + Require(merge(base, "a?q=1#f", "http://foo.com/foo/a?q=1#f")); + Require(merge("https://u:p@foo.com:8443/a/b?x=1#top", "c", "https://u:p@foo.com:8443/a/c")); + Require(merge("http://foo.com", "x", "http://foo.com/x")); + Require(merge("http://foo.com:80/", "x", "http://foo.com/x")); + Require(merge("http://foo.com/a/b", "mailto:x", "http://foo.com/a/mailto:x")); + + Require(merge(base, "http://bar.com/other", "http://bar.com/other") && required == 21); + const std::size_t absoluteSize = baseMergeSize + 2 * (29 + 20); + for (std::size_t i = 21; i < absoluteSize; ++i) Require(merged[i] == '\0'); + Require(merged[absoluteSize] == 'Z'); + Require(merge(base, "//bar.com/x", "//bar.com/x") && required == 12); + + required = 0; + Require(sceHttpUriMerge(nullptr, base, "./default.html", &required, 0, 0) == 0); + Require(required == baseMergeSize + 2 * (29 + 14)); + Require(sceHttpUriMerge(nullptr, base, "http://bar.com/other", &required, 0, 0) == 0); + Require(required == absoluteSize); + Require(sceHttpUriMerge(nullptr, base, "x", nullptr, 0, 0) == 0); + std::memset(merged, 'Z', sizeof(merged)); + Require(sceHttpUriMerge(merged, base, "./default.html", &required, baseMergeSize + 2 * (29 + 14) - 1, 0) == outOfMemory); + Require(required == baseMergeSize + 2 * (29 + 14) && merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, base, "http://bar.com/other", &required, absoluteSize - 1, 0) == outOfMemory); + Require(merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, base, "./default.html", nullptr, baseMergeSize + 2 * (29 + 14), 0) == 0); + Require(Equal(merged, "http://foo.com/foo/./default.html")); + + required = 123; + Require(sceHttpUriMerge(merged, nullptr, "./x", &required, sizeof(merged), 0) == invalidValue); + Require(sceHttpUriMerge(merged, base, nullptr, &required, sizeof(merged), 0) == invalidValue); + Require(sceHttpUriMerge(merged, base, "./x", &required, sizeof(merged), 1) == invalidValue); + Require(sceHttpUriMerge(nullptr, nullptr, nullptr, &required, 0, 1) == invalidValue); + Require(sceHttpUriMerge(merged, "http://bad host/", "./x", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); + Require(required == 123); + HttpEpollHandle epoll = nullptr; Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..5f71fe0b4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -109,6 +109,7 @@ Throughout the project, every function at every stage either **does exactly what - [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read +- [sceHttpUriMerge](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature From 16285b473f9a16ddfc01708867badc9ca3823c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 11:51:34 +0200 Subject: [PATCH 055/203] libSceHttp: classify the relative reference without host validation in sceHttpUriMerge Co-Authored-By: Claude --- core/libs/prx/libSceHttp/src/Uri.cpp | 22 +++++++++++++++++----- core/libs/tests/GuestHttp.cpp | 4 ++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 68c4e3c71..b0e54b7f7 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -74,14 +74,23 @@ std::string removeDotSegments(std::string_view input) { return output; } -int parseUri(std::string_view uri, UriParts& parts) { +size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; if (!uri.empty() && std::isalpha(static_cast(uri[0]))) { schemeEnd = 1; while (schemeEnd < uri.size() && (std::isalnum(static_cast(uri[schemeEnd])) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; } - if (schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':') { + return schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':' ? schemeEnd : 0; +} + +bool isOpaque(std::string_view uri) { + const size_t schemeEnd = schemeLength(uri); + return !uri.substr(schemeEnd == 0 ? 0 : schemeEnd + 1).starts_with("//"); +} + +int parseUri(std::string_view uri, UriParts& parts) { + if (const size_t schemeEnd = schemeLength(uri); schemeEnd != 0) { parts.scheme = uri.substr(0, schemeEnd); uri.remove_prefix(schemeEnd + 1); } @@ -232,8 +241,10 @@ int APS5_VABI sceHttpUriMerge(char* merged_url, const char* url, const char* rel UriParts base; if (const int result = parseUri(url, base); result != 0) return result; - UriParts relative; - if (const int result = parseUri(relative_uri, relative); result != 0) return result; + const bool relativeOpaque = isOpaque(relative_uri); + if (UriParts relative; !relativeOpaque) { + if (const int result = parseUri(relative_uri, relative); result != 0) return result; + } const size_t urlLength = strnlen(url, URI_MAX_LENGTH); const size_t relativeLength = strnlen(relative_uri, URI_MAX_LENGTH); @@ -242,8 +253,9 @@ int APS5_VABI sceHttpUriMerge(char* merged_url, const char* url, const char* rel if (!merged_url) return 0; if (prepare < size) return ERROR_OUT_OF_MEMORY; - if (!relative.opaque) { + if (!relativeOpaque) { std::strncpy(merged_url, relative_uri, size); + merged_url[size - 1] = '\0'; if (require) *require = relativeLength + 1; return 0; } diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 4dbcc057f..7c63fb6da 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -143,6 +143,10 @@ int main() { Require(merge("http://foo.com", "x", "http://foo.com/x")); Require(merge("http://foo.com:80/", "x", "http://foo.com/x")); Require(merge("http://foo.com/a/b", "mailto:x", "http://foo.com/a/mailto:x")); + Require(merge(base, "a%20b.html", "http://foo.com/foo/a%20b.html")); + Require(merge(base, "~user/x", "http://foo.com/foo/~user/x")); + Require(merge(base, "file(1).png", "http://foo.com/foo/file(1).png")); + Require(merge(base, "a+b=c;d!e", "http://foo.com/foo/a+b=c;d!e")); Require(merge(base, "http://bar.com/other", "http://bar.com/other") && required == 21); const std::size_t absoluteSize = baseMergeSize + 2 * (29 + 20); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5f71fe0b4..fee12ac08 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -109,7 +109,7 @@ Throughout the project, every function at every stage either **does exactly what - [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read -- [sceHttpUriMerge](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. +- [sceHttpUriMerge](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. The relative reference is only classified by its scheme and a leading `//`, and only references with `//` are validated; as in shadPS4, `//host/x` is returned unchanged instead of taking the base scheme, a reference with a scheme but no `//` (`mailto:x`) is joined onto the base directory, and `?q` and `#f` are appended to the base directory instead of the base path. - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature From 92d79996c777932b2ab1a5c8bbdc10da26d4cabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 10:26:36 +0200 Subject: [PATCH 056/203] libSceNet: implement sceNetSendmsg and sceNetRecvmsg Co-Authored-By: Claude --- core/libs/prx/libSceNet/Export.cpp | 100 +++++++++++++++++++++++++++++ core/libs/tests/GuestSceNet.cpp | 93 +++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 194 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0aa78dcd6..bd02f7958 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -37,6 +38,7 @@ namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; constexpr int NET_EBADF = 9; +constexpr int NET_EFAULT = 14; constexpr int NET_EINVAL = 22; constexpr int NET_ENOSPC = 28; constexpr int NET_EAGAIN = 35; @@ -62,6 +64,9 @@ constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_NBIO = 0x1200; +constexpr int NET_MSG_PEEK = 0x2; +constexpr int NET_MSG_TRUNC = 0x10; +constexpr int NET_UIO_MAXIOV = 1024; #ifdef _WIN32 using NativeSocket = SOCKET; @@ -247,6 +252,37 @@ std::uint64_t swap64(std::uint64_t v) { ((v >> 8) & 0xFF000000u) | ((v >> 24) & 0xFF0000u) | ((v >> 40) & 0xFF00u) | (v >> 56); } +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t name_length; + NetIovec* iov; + int iov_length; + void* control; + std::uint32_t control_length; + int flags; +}; +static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, control) == 32 && + offsetof(NetMsghdr, flags) == 44); + +std::int64_t message_length(const NetMsghdr* message) { + if (!message) return fail(NET_EFAULT); + if (message->iov_length < 0 || message->iov_length > NET_UIO_MAXIOV) return fail(NET_EMSGSIZE); + if (message->iov_length && !message->iov) return fail(NET_EFAULT); + std::uint64_t total = 0; + for (int i = 0; i < message->iov_length; ++i) { + const auto& entry = message->iov[i]; + if (!entry.base && entry.length) return fail(NET_EFAULT); + if (entry.length > INT_MAX - total) return fail(NET_EINVAL); + total += entry.length; + } + return static_cast(total); +} + } // namespace extern "C" { @@ -525,6 +561,70 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co return result >= 0 ? result : fail(native_error()); } +int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { + const auto total = message_length(msg); + if (total < 0) return total; + if (msg->control && msg->control_length) throw std::runtime_error("sceNetSendmsg: control data is not supported"); + std::vector buffer; + try { + buffer.resize(static_cast(total)); + } catch (const std::bad_alloc&) { + return fail(55); + } + std::size_t offset = 0; + for (int i = 0; i < msg->iov_length; ++i) { + if (msg->iov[i].length) std::memcpy(buffer.data() + offset, msg->iov[i].base, msg->iov[i].length); + offset += msg->iov[i].length; + } + return sceNetSendto(s, buffer.data(), buffer.size(), flags, msg->name, msg->name ? msg->name_length : 0u); +} + +int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { + const auto total = message_length(msg); + if (total < 0) return total; + if (flags != 0 && flags != NET_MSG_PEEK) return fail(NET_EOPNOTSUPP); + Sock socket; + { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) return fail(NET_EBADF); + socket = it->second; + } + const bool datagram = socket.type != NET_SOCK_STREAM; + std::vector buffer; + try { + buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); + } catch (const std::bad_alloc&) { + return fail(55); + } + sockaddr_storage peer{}; + NativeLength peer_length = sizeof(peer); + std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), + flags == NET_MSG_PEEK ? MSG_PEEK : 0, reinterpret_cast(&peer), &peer_length); + if (received < 0) { +#ifdef _WIN32 + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); + received = static_cast(buffer.size()); +#else + return fail(native_error()); +#endif + } + msg->flags = 0; + if (received > total) { + received = total; + msg->flags = NET_MSG_TRUNC; + } + std::size_t offset = 0; + for (int i = 0; i < msg->iov_length && offset < static_cast(received); ++i) { + const auto count = std::min(msg->iov[i].length, static_cast(received) - offset); + std::memcpy(msg->iov[i].base, buffer.data() + offset, count); + offset += count; + } + if (msg->name && !native_to_guest_address(peer, msg->name, &msg->name_length)) msg->name_length = 0; + msg->control_length = 0; + return received; +} + int APS5_VABI sceNetShutdown(int s, int how) { if (how < 0 || how > 2) return fail(NET_EINVAL); Sock socket; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index fe213c179..4bbb122ce 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -36,10 +37,91 @@ const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); int* APS5_VABI sceNetErrnoLoc(void); } +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t name_length; + NetIovec* iov; + int iov_length; + void* control; + std::uint32_t control_length; + int flags; +}; + +extern "C" { +std::int64_t APS5_VABI sceNetSendmsg(int, const NetMsghdr*, int); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); +} + static void Require(bool condition) { if (!condition) std::abort(); } +static bool Failed(std::int64_t result, int error) { + return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; +} + +static void CheckMessages(int receiver, int sender, const std::array& address) { + char head[] = "scatter "; + char tail[] = "gather"; + NetIovec out[] = {{head, 8}, {nullptr, 0}, {tail, 6}}; + std::array destination = address; + NetMsghdr send{destination.data(), 16, out, 3, nullptr, 0, 0}; + Require(sceNetSendmsg(sender, &send, 0) == 14); + + char first[5]{}; + char second[20]{}; + NetIovec in[] = {{first, sizeof(first)}, {second, sizeof(second)}}; + std::array source{}; + char control[16]{}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 2, control, sizeof(control), -1}; + Require(sceNetRecvmsg(receiver, &receive, 2) == 14); + Require(std::memcmp(first, "scatt", 5) == 0 && std::memcmp(second, "er gather", 9) == 0); + Require(receive.flags == 0 && receive.control_length == 0 && receive.name_length == 16); + Require(source[0] == 16 && source[1] == 2 && source[4] == 127 && source[7] == 1); + std::memset(second, 0, sizeof(second)); + receive.iov_length = 1; + receive.flags = -1; + Require(sceNetRecvmsg(receiver, &receive, 0) == 5); + Require(receive.flags == 0x10 && std::memcmp(first, "scatt", 5) == 0 && second[0] == 0); + + NetMsghdr bad = send; + Require(Failed(sceNetSendmsg(sender, nullptr, 0), 14)); + Require(Failed(sceNetRecvmsg(receiver, nullptr, 0), 14)); + bad.iov_length = -1; + Require(Failed(sceNetSendmsg(sender, &bad, 0), 40)); + bad.iov_length = 1025; + Require(Failed(sceNetRecvmsg(receiver, &bad, 0), 40)); + bad.iov_length = 1; + bad.iov = nullptr; + Require(Failed(sceNetSendmsg(sender, &bad, 0), 14)); + NetIovec hole[] = {{nullptr, 1}}; + bad.iov = hole; + Require(Failed(sceNetRecvmsg(receiver, &bad, 0), 14)); + NetIovec huge[] = {{head, 0x7fffffff}, {tail, 1}}; + bad.iov = huge; + bad.iov_length = 2; + Require(Failed(sceNetSendmsg(sender, &bad, 0), 22)); + Require(Failed(sceNetRecvmsg(receiver, &receive, 1), 45)); + Require(Failed(sceNetSendmsg(sender, &send, 1), 45)); + Require(Failed(sceNetRecvmsg(12345, &receive, 0), 9)); + Require(Failed(sceNetSendmsg(12345, &send, 0), 9)); + NetMsghdr with_control = send; + with_control.control = control; + with_control.control_length = sizeof(control); + bool threw = false; + try { + sceNetSendmsg(sender, &with_control, 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} + static void CheckAddressText(int family, const char* text) { std::array address{}; Require(sceNetInetPton(family, text, address.data()) == 1); @@ -103,6 +185,16 @@ int main() { Require(ready_count == 1 && (ready.events & 1) && ready.ident == static_cast(accepted)); Require(sceNetRecv(accepted, response, sizeof(response), 0) == sizeof(response)); Require(std::strcmp(request, response) == 0); + char reply[] = "stream reply"; + NetIovec reply_out[] = {{reply, 7}, {reply + 7, sizeof(reply) - 7}}; + NetMsghdr reply_send{nullptr, 0, reply_out, 2, nullptr, 0, 0}; + Require(sceNetSendmsg(accepted, &reply_send, 0) == sizeof(reply)); + char reply_result[sizeof(reply)]{}; + NetIovec reply_in[] = {{reply_result, sizeof(reply_result)}}; + std::array reply_name{}; + NetMsghdr reply_receive{reply_name.data(), 16, reply_in, 1, nullptr, 0, -1}; + Require(sceNetRecvmsg(client, &reply_receive, 0) == sizeof(reply)); + Require(std::strcmp(reply, reply_result) == 0 && reply_receive.flags == 0 && reply_receive.name_length == 0); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); bool send_failed = false; @@ -130,6 +222,7 @@ int main() { Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, source.data(), &address_size) == sizeof(datagram_result)); Require(std::strcmp(datagram, datagram_result) == 0 && source[1] == 2); + CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); Require(sceNetSocketClose(udp_receiver) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..b467e3ce3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -135,6 +135,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. From 9bcd6767749d6a64a4448fd58b2dda8dc1eac337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 11:51:42 +0200 Subject: [PATCH 057/203] libSceNet: link libc into the guest net test for MinGW exception handling Co-Authored-By: Claude --- core/libs/prx/libSceNet/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index d6a6254c7..4eac17f68 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -11,7 +11,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl) + target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libc) configure_windows_unwind(guest_sce_net_tests) add_test(NAME guest_sce_net COMMAND guest_sce_net_tests) set_tests_properties(guest_sce_net PROPERTIES TIMEOUT 20) From 7db0a80b196399125e2bf81f00cdee87ccfb91e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 11:51:26 +0200 Subject: [PATCH 058/203] libSceHttp: keep sceHttpUriSweepPath inside srcSize, cite shadPS4 Co-Authored-By: Claude --- core/libs/prx/libSceHttp/src/Uri.cpp | 11 +++++------ core/libs/tests/GuestHttp.cpp | 4 ++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index b9a300ea4..7441f6ca7 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -260,7 +259,7 @@ int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { if (!dst || !src) return ERROR_INVALID_VALUE; const size_t length = srcSize - 1; - if (src[0] != '/') { + if (length == 0 || src[0] != '/') { std::memcpy(dst, src, length); dst[length] = '\0'; return 0; @@ -271,11 +270,11 @@ int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { size_t end = 0; size_t pos = 1; while (pos < length) { - if (src[pos] == '.' && src[pos + 1] == '/') { + if (src[pos] == '.' && pos + 1 < length && src[pos + 1] == '/') { pos += 2; continue; } - if (src[pos] == '.' && src[pos + 1] == '.' && src[pos + 2] == '/') { + if (src[pos] == '.' && pos + 2 < length && src[pos + 1] == '.' && src[pos + 2] == '/') { if (end != 0) { dst[end] = '\0'; end = static_cast(std::strrchr(dst, '/') - dst); @@ -285,8 +284,8 @@ int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { continue; } size_t count = length - pos; - if (const char* slash = std::strchr(src + pos, '/')) { - count = std::min(static_cast(slash + 1 - (src + pos)), count); + if (const void* slash = std::memchr(src + pos, '/', length - pos)) { + count = static_cast(static_cast(slash) + 1 - (src + pos)); } std::memcpy(dst + end + 1, src + pos, count); dst[end + 1 + count] = '\0'; diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 75d86a904..9df650f62 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -217,4 +217,8 @@ int main() { Require(sweep("/a/..b/.c", "/a/..b/.c")); Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 6) == 0 && Equal(sweptPath, "/a/b/")); Require(sceHttpUriSweepPath(sweptPath, "/ab/c", 3) == 0 && Equal(sweptPath, "/a")); + Require(sceHttpUriSweepPath(sweptPath, "/a/./b", 5) == 0 && Equal(sweptPath, "/a/.")); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 8) == 0 && Equal(sweptPath, "/a/b/..")); + char sweptByte[2] = {'x', 'x'}; + Require(sceHttpUriSweepPath(sweptByte, "/", 1) == 0 && sweptByte[0] == '\0' && sweptByte[1] == 'x'); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..61298c006 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -110,6 +110,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. +- [sceHttpUriSweepPath](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - behaviour follows the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library and its tests (including the PS4 SDK example), not confirmed on a PS5 title or hardware. It is not RFC 3986 5.2.4: a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` without a slash is kept. Unlike shadPS4, nothing at or past `srcSize - 1` is read, and `srcSize == 1` writes only the terminator - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From e14184b915bf3c7ce8b4719eb3d0b6ed11ba5825 Mon Sep 17 00:00:00 2001 From: ebosido444-ai <316167330+ebosido444-ai@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:11:41 +0300 Subject: [PATCH 059/203] feat(libc): implement std_execute_once The plain-named twin of std::_Execute_once delegates to that symbol and inherits its call_once semantics; the name-equivalence assumption is recorded in TechnicalDebt. Registers guest_std_execute_once_tests. --- core/libs/prx/libc/CMakeLists.txt | 10 ++++ core/libs/prx/libc/Export.cpp | 10 ++-- core/libs/tests/GuestStdExecuteOnce.cpp | 64 +++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 core/libs/tests/GuestStdExecuteOnce.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 575971f71..546478219 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -101,6 +101,16 @@ if(BUILD_TESTING) set_tests_properties(guest_execute_once PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_std_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdExecuteOnce.cpp) + target_include_directories(guest_std_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_std_execute_once_tests PRIVATE libc) + configure_windows_unwind(guest_std_execute_once_tests) + add_test(NAME guest_std_execute_once COMMAND guest_std_execute_once_tests) + set_tests_properties(guest_std_execute_once PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_std_execute_once PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index bdb6ca001..ebbb25bea 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -8,12 +8,10 @@ uint32_t Need_sceLibc = 1; extern "C" { - int APS5_VABI std_execute_once_nid_postfix(int* flag, int (*func)(void*, void*, void**), void* arg) { - (void)flag; - (void)func; - (void)arg; - NotImplemented_nid_no_patch(__func__); - return 0; + int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(int*, int (APS5_VABI *)(void*, void*, void**), void*); + + int APS5_VABI std_execute_once_nid_postfix(int* flag, int (APS5_VABI *func)(void*, void*, void**), void* arg) { + return _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(flag, func, arg); } void APS5_VABI LibcHeapGetTraceInfo_nid_postfix(LibcHeapInfo* info) { diff --git a/core/libs/tests/GuestStdExecuteOnce.cpp b/core/libs/tests/GuestStdExecuteOnce.cpp new file mode 100644 index 000000000..d189ddcd7 --- /dev/null +++ b/core/libs/tests/GuestStdExecuteOnce.cpp @@ -0,0 +1,64 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +using Callback = int (APS5_VABI *)(void*, void*, void**); +extern "C" int APS5_VABI std_execute_once_nid_postfix(int*, Callback, void*); +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}, arrived{0}; +static int control = 0, nested = 0, failing = 0, throwing = 0; +static int published = 0, nestedValue = 0, failures = 0, throws = 0; +static int APS5_VABI Nested(void*, void* arg, void**) { + nestedValue = *static_cast(arg); + return 1; +} +static int APS5_VABI Initialize(void* first, void* arg, void** third) { + Require(first == nullptr && third == nullptr && arg != nullptr); + ++calls; + while (arrived.load() != 16) std::this_thread::yield(); + int value = 17; + Require(std_execute_once_nid_postfix(&nested, Nested, &value) == 1); + published = 42; + return 5; +} +static int APS5_VABI Fail(void*, void*, void**) { + ++failures; + return 0; +} +static int APS5_VABI Throw(void*, void*, void**) { + if (++throws == 1) throw 7; + return 1; +} +static int APS5_VABI Unreachable(void*, void*, void**) { + std::abort(); +} + +int main() { + int token = 0; + std::array workers; + for (auto& worker : workers) worker = std::thread([&token] { + ++arrived; + Require(std_execute_once_nid_postfix(&control, Initialize, &token) == 1); + Require(published == 42 && nestedValue == 17); + }); + for (auto& worker : workers) worker.join(); + Require(calls == 1 && control == 1 && nested == 1); + Require(std_execute_once_nid_postfix(&control, Unreachable, nullptr) == 1 && calls == 1); + + Require(std_execute_once_nid_postfix(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 1); + Require(std_execute_once_nid_postfix(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 2); + + try { + std_execute_once_nid_postfix(&throwing, Throw, nullptr); + std::abort(); + } catch (int value) { + Require(value == 7 && throwing == 0); + } + Require(std_execute_once_nid_postfix(&throwing, Throw, nullptr) == 1 && throwing == 1 && throws == 2); + + int preset = -3; + Require(std_execute_once_nid_postfix(&preset, Unreachable, nullptr) == 1 && preset == -3); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..017e8a040 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -73,6 +73,7 @@ Throughout the project, every function at every stage either **does exactly what - [std::_Fiopen](../../core/libs/prx/libc/src/CxxAbiSupport.cpp) (libc) - `_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi` (NID `vYWK2Pz8vGE`; the name comes from the public NID list): the values of the `_Openmode` argument and the meaning of the protection argument are unknown, so it throws when called. It is exported so that a title that imports it (PPSA21564 and PPSA21567 do) loads against the host libc when its own `libc.prx` is left out of the guest conversion. - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso +- [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 - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED throws: FreeBSD 9.0 libthr rejects it with EINVAL and 11.0 accepts it, and which one the console follows is unknown - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature From 6b69c70a02d9e9ede5f1739acf84c34c09fea69b Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 6 Oct 2026 12:31:30 +0200 Subject: [PATCH 060/203] feat(font): implement sceFontWritingLineGetOrderingSpace Report the spacing the writing line applies: the modelled line places each run at the sum of the previous advances, so every space is 0. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/Unimplemented.cpp | 5 ----- core/libs/prx/libSceFont/src/WritingLine.cpp | 10 ++++++++++ core/libs/tests/GuestFontWritingLine.cpp | 16 ++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index e7a004051..6a26c7724 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -422,11 +422,6 @@ int APS5_VABI sceFontWordsFindWordCharacters() { return 0; } -int APS5_VABI sceFontWritingLineGetOrderingSpace() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFont/src/WritingLine.cpp b/core/libs/prx/libSceFont/src/WritingLine.cpp index 5a313c994..216a03deb 100644 --- a/core/libs/prx/libSceFont/src/WritingLine.cpp +++ b/core/libs/prx/libSceFont/src/WritingLine.cpp @@ -124,6 +124,16 @@ int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingM return SCE_FONT_OK; } +int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* headSpace, float* inlineSpace, float* tailSpace, float* advanceSpace) { + GetLine(__func__, writingLine); + if (!headSpace || !inlineSpace || !tailSpace || !advanceSpace) Unsupported(__func__, "null space pointer (error code not verified)"); + *headSpace = 0.0f; + *inlineSpace = 0.0f; + *tailSpace = 0.0f; + *advanceSpace = 0.0f; + return SCE_FONT_OK; +} + } #pragma GCC visibility pop diff --git a/core/libs/tests/GuestFontWritingLine.cpp b/core/libs/tests/GuestFontWritingLine.cpp index 40281cd0f..7d5e40f08 100644 --- a/core/libs/tests/GuestFontWritingLine.cpp +++ b/core/libs/tests/GuestFontWritingLine.cpp @@ -11,6 +11,7 @@ int APS5_VABI sceFontWritingLineClear(void*); int APS5_VABI sceFontWritingLineWritesOrder(void*, std::uint64_t, const FontWritingMetrics*, void*); const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void*); int APS5_VABI sceFontWritingLineGetRenderMetrics(void*, FontWritingMetrics*); +int APS5_VABI sceFontWritingLineGetOrderingSpace(void*, float*, float*, float*, float*); } static void Check(bool value, int line) { @@ -42,6 +43,15 @@ static bool Same(const FontWritingMetrics& a, const FontWritingMetrics& b) { a.Extent.bottom == b.Extent.bottom && a.Extent.left == b.Extent.left && a.Extent.right == b.Extent.right; } +static bool NoSpace(void* line) { + float head = 1.0f; + float inner = 1.0f; + float tail = 1.0f; + float advance = 1.0f; + return sceFontWritingLineGetOrderingSpace(line, &head, &inner, &tail, &advance) == SCE_FONT_OK && head == 0.0f && inner == 0.0f && + tail == 0.0f && advance == 0.0f; +} + static bool StepIs(const FontWritingLineStep* step, float x, const FontWritingMetrics& run, void* orderer) { return step && step->x == x && step->y == 0.0f && step->advanceX == run.advanceX && step->advanceY == run.advanceY && step->spacingProgress == 0.0f && step->writingOrderer == orderer && step->Adjusting.x == 0.0f && @@ -62,6 +72,7 @@ int main() { Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, <r) == SCE_FONT_OK && ltr != nullptr && ltr != line); FontWritingMetrics metrics{}; + Require(NoSpace(line)); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, &metrics); })); @@ -74,6 +85,10 @@ int main() { Require(sceFontWritingLineWritesOrder(line, 0, &runB, &ordererB) == SCE_FONT_OK); Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK); Require(Same(metrics, {16.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 15.0f}})); + Require(NoSpace(line)); + float space = 0.0f; + Require(Throws([&] { sceFontWritingLineGetOrderingSpace(line, nullptr, &space, &space, &space); })); + Require(Throws([&] { sceFontWritingLineGetOrderingSpace(line, &space, &space, &space, nullptr); })); Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runA, &ordererA)); Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, runB, &ordererB)); @@ -109,6 +124,7 @@ int main() { Require(Throws([&] { sceFontWritingLineClear(nullptr); })); Require(Throws([&] { sceFontWritingLineClear(¬ALine); })); Require(Throws([&] { sceFontWritingLineRefersRenderStep(¬ALine); })); + Require(Throws([&] { sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space); })); void* bogus = ¬ALine; Require(Throws([&] { sceFontDestroyWritingLine(&bogus); }) && bogus == ¬ALine); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..1c0124875 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -48,6 +48,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown +- [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The modelled line applies no spacing, so the head, inline, tail and advance spaces are 0; what spacing the console applies is unknown. A null output throws - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From 29888493b5242d2a6c13814ebf80fb8032271e84 Mon Sep 17 00:00:00 2001 From: ahirm-finarth Date: Tue, 6 Oct 2026 16:41:45 +0530 Subject: [PATCH 061/203] fix(shader): preserve pixel export mappings in replay --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 210 ++++++++++++++++++ .../ControlFlow/src/RequestSerializer.cpp | 14 +- 2 files changed, 222 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a65c7d9e5..383a9eb77 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -601,6 +602,212 @@ ShaderRecompiler::ShaderPixelStageInfo twoParameterPixel() { return pixel; } +std::string requestPrefix(std::string_view text, std::size_t bytes) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string result(text.substr(0, ((bytes + 2u) / 3u) * 4u)); + if (bytes % 3u == 1u) { + result[result.size() - 3u] = alphabet[alphabet.find(result[result.size() - 3u]) & 0x30u]; + result[result.size() - 2u] = '='; + result.back() = '='; + } else if (bytes % 3u == 2u) { + result[result.size() - 2u] = alphabet[alphabet.find(result[result.size() - 2u]) & 0x3cu]; + result.back() = '='; + } + return result; +} + +void verifyPixelRequestSerialization() { + using namespace ShaderRecompiler; + const auto fields = [](const ShaderPixelStageInfo& value) { + return std::tie(value.interpolatorCount, value.interpolatorSettings, value.wave32, value.inputAddr, + value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY, + value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, + value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, + value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, + value.targetOutputMode, value.targetExportMapping); + }; + const std::array code{0xbf810000u}; + const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; + const std::array memory{{{0x60000u, std::as_bytes(std::span(userData))}}}; + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userData = userData; + request.context.memory = memory; + request.context.vertex = ShaderVertexStageInfo{}; + request.context.vertex->fetchAttribReg = 17u; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.nonConstantImageOffsets = true; + request.layout = {0u, 11u, 16u, 128u}; + request.useCache = false; + ShaderPixelStageInfo pixel{ + .interpolatorCount = 32u, + .wave32 = true, + .inputAddr = 0x7fffu, + .hasPerspectiveCenterVgpr = true, + .perspectiveCentroid = true, + .posX = true, + .posY = true, + .posZ = true, + .posW = true, + .frontFace = true, + .ancillary = true, + .sampleShading = true, + .noPerspective = true, + .linearCentroid = true, + .pixelKillEnable = true, + .depthExportEnable = true, + .sampleMaskExportEnable = true, + .earlyZ = true, + .executeOnNoop = true, + .conservativeZExport = ConservativeZExport::GreaterThanZ, + .targetOutputMode = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u} + }; + for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; + const std::array, 3> mappings{{ + {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, + {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, + {0xc6u, 0x1bu, 0x00u, 0xffu, 0x55u, 0xaau, 0x80u, 0x39u} + }}; + const RequestSerializer serializer; + for (const auto& mapping : mappings) { + pixel.targetExportMapping = mapping; + request.context.pixel = pixel; + const auto replay = serializer.Deserialize(serializer.Serialize(request)); + require(replay.request.context.pixel.has_value() && fields(*replay.request.context.pixel) == fields(pixel), "pixel fields or export mappings were lost in serialization"); + require(replay.request.context.vertex.has_value() && replay.request.context.vertex->fetchAttribReg == 17u && replay.request.context.memory.size() == 1u && replay.request.context.memory[0].guestAddress == 0x60000u && replay.request.context.memory[0].bytes.size() == sizeof(userData), "pixel mappings displaced the following guest context"); + require(replay.request.target.subgroupSize == 64u && replay.request.target.nonConstantImageOffsets && replay.request.layout.firstBinding == 11u && replay.request.layout.pushConstantOffsetBytes == 16u && !replay.request.useCache, "pixel mappings displaced the following request fields"); + std::vector key; + RecompileCacheKey::Build(request, key); + std::vector replayKey; + RecompileCacheKey::Build(replay.request, replayKey); + require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(replay.request), "pixel replay changed shader identity"); + for (std::size_t i = 0; i < mapping.size(); ++i) { + auto changed = request; + changed.context.pixel->targetExportMapping[i] ^= 1u; + RecompileCacheKey::Build(changed, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(changed), "shader identity ignored a pixel export mapping"); + } + } + request.context.pixel.reset(); + request.shader.stage = ShaderStage::Vertex; + const auto withoutPixel = serializer.Deserialize(serializer.Serialize(request)); + require(!withoutPixel.request.context.pixel.has_value() && withoutPixel.request.context.vertex.has_value() && withoutPixel.request.context.vertex->fetchAttribReg == 17u && withoutPixel.request.context.memory.size() == 1u && withoutPixel.request.context.memory[0].guestAddress == 0x60000u && withoutPixel.request.target.nonConstantImageOffsets && withoutPixel.request.layout.firstBinding == 11u && !withoutPixel.request.useCache, "a request without pixel state was misaligned"); + + RecompileRequest minimal{}; + minimal.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + minimal.context.waveSize = 64; + minimal.context.pixel = ShaderPixelStageInfo{}; + const auto encoded = serializer.Serialize(minimal); + require(requestPrefix(encoded, 8u) == "NVNQQQgAAAA=", "new requests did not use serialization version 8"); + constexpr std::size_t mappingOffset = 8u + 37u + 18u + 162u; + for (std::size_t bytes = 0; bytes < 8u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-8 pixel mapping was accepted"); + } + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQkAAAA="}) { + expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); + } +} + +void verifyLegacyPixelRequests() { + using namespace ShaderRecompiler; + static constexpr std::array legacyPixelRequests{ + "NVNQQQEAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" + "AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA" + "AAAA", + "NVNQQQIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" + "AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA" + "AAAAAA==", + "NVNQQQMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" + "AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA" + "AAAAAA==", + "NVNQQQQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" + "AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA" + "AAAAAA==", + "NVNQQQUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAA" + "AAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAA" + "gAAAAAAA", + "NVNQQQYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAA" + "AAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAA" + "gAAAAAAAAQ==", + "NVNQQQcAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" + "AAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAA" + "AIAAAAAAAAE=", + }; + const RequestSerializer serializer; + for (std::size_t index = 0; index < legacyPixelRequests.size(); ++index) { + const auto version = index + 1u; + const auto replay = serializer.Deserialize(legacyPixelRequests[index]); + const auto& request = replay.request; + require(request.context.pixel.has_value(), "legacy pixel state was lost"); + const auto& pixel = *request.context.pixel; + require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); + require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); + require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); + require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); + require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u), "legacy request trailer was misread"); + const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); + require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); + } +} + +void verifyPixelExportReplay() { + using namespace ShaderRecompiler; + std::array code{ + 0x7e0002ffu, 0x3e800000u, 0x7e0202ffu, 0x3f000000u, + 0x7e0402ffu, 0x3f400000u, 0x7e0602ffu, 0x3f800000u, + 0xf800180fu, 0x03020100u, 0xbf810000u + }; + const RequestSerializer serializer; + for (const auto target : {0u, 7u}) { + code[8] = 0xf800180fu | (target << 4u); + ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[target] = 9u; + pixel.targetExportMapping.fill(0xe4u); + pixel.targetExportMapping[target] = 0xc6u; + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64u; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64u; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + const auto replay = serializer.Deserialize(serializer.Serialize(request)); + const auto original = Recompile(request); + auto identity = request; + identity.context.pixel->targetExportMapping[target] = 0xe4u; + require(original.spirv != Recompile(identity).spirv, "the non-identity pixel export mapping did not affect the compiled shader"); + require(replay.request.context.pixel.has_value() && replay.request.context.pixel->targetExportMapping == pixel.targetExportMapping, "replay lost the non-identity pixel export mapping"); + const auto replayed = Recompile(replay.request); + verifyResult(original, replayed); + request.useCache = true; + const auto cached = Recompile(request); + const auto cachedReplay = serializer.Deserialize(serializer.Serialize(request)); + const auto hit = Recompile(cachedReplay.request); + require(hit.cacheHit && hit.variantId == cached.variantId, "pixel replay did not reuse the original shader variant"); + verifyResult(cached, hit); + } +} + std::vector noPerspectiveLocations(std::span code) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -1044,6 +1251,9 @@ int main() { verifyProgramCounterRelativeData(); verifyMeshConfiguration(); verifyPixelInputs(); + verifyPixelRequestSerialization(); + verifyLegacyPixelRequests(); + verifyPixelExportReplay(); verifyPixelParameterSlots(); verifyComputedTexelOffsets(); verifyWaveUniformValues(); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 0e22804d0..d6c4a5750 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -308,6 +308,9 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { for (const std::uint8_t value : info.targetOutputMode) { writer.WriteU8(value); } + for (const std::uint8_t value : info.targetExportMapping) { + writer.WriteU8(value); + } } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -341,6 +344,13 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { for (std::uint8_t& value : info.targetOutputMode) { value = reader.ReadU8(); } + if (version >= 8u) { + for (std::uint8_t& value : info.targetExportMapping) { + value = reader.ReadU8(); + } + } else { + info.targetExportMapping.fill(0u); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -687,7 +697,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(7u); + writer.WriteU32(8u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -711,7 +721,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 7u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 8u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); From 8ef51df3037fae9502cb3ac82b494685d7edde1e Mon Sep 17 00:00:00 2001 From: Aryan Rawat <235018975+aryanrawatispro-ship-it@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:42:02 +0000 Subject: [PATCH 062/203] feat(playerinvitationdialog): implement scePlayerInvitationDialogGetResult scePlayerInvitationDialogGetResult makes the common dialog checks of sceWebBrowserDialogGetResult: NOT_INITIALIZED before the dialog is initialized, ARG_NULL for a null result and NOT_FINISHED until the dialog finishes. The dialog shows no UI and sends no invitation, so the finished dialog reports USER_CANCELED with error code 0. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from SharpProspero. Co-Authored-By: Claude Opus 5.5 --- .../libScePlayerInvitationDialog.cpp | 16 +++++++++++++--- .../libScePlayerInvitationDialog.h | 9 ++++++++- core/libs/tests/PlayerInvitationDialog.cpp | 16 ++++++++++++++++ docs/dev/TechnicalDebt.md | 4 ++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp index 104080043..36965cc8e 100644 --- a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp @@ -6,6 +6,11 @@ namespace { +constexpr std::int32_t COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr auto COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr auto COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr auto COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); + std::mutex g_dialog_mutex; PlayerInvitationDialogStatus g_dialog_status = PlayerInvitationDialogStatus::None; @@ -67,9 +72,14 @@ std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void) { return 0; } -std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(ScePlayerInvitationDialogResult* result) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("result=%p status=%d", static_cast(result), status()); + if (g_dialog_status == PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_dialog_status != PlayerInvitationDialogStatus::Finished) return COMMON_DIALOG_ERROR_NOT_FINISHED; + *result = ScePlayerInvitationDialogResult{}; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; return 0; } diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h index a8a4d2724..a52d8dd90 100644 --- a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h @@ -16,6 +16,13 @@ struct ScePlayerInvitationDialogParam { std::uint8_t reserved[1]; }; +struct ScePlayerInvitationDialogResult { + std::int32_t errorCode; + std::int32_t result; + std::uint8_t reserved[32]; +}; +static_assert(sizeof(ScePlayerInvitationDialogResult) == 40); + extern "C" { std::int32_t APS5_VABI scePlayerInvitationDialogInitialize(void); @@ -24,7 +31,7 @@ std::int32_t APS5_VABI scePlayerInvitationDialogUpdateStatus(void); std::int32_t APS5_VABI scePlayerInvitationDialogGetStatus(void); std::int32_t APS5_VABI scePlayerInvitationDialogClose(void); std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void); -std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(void* result); +std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(ScePlayerInvitationDialogResult* result); } diff --git a/core/libs/tests/PlayerInvitationDialog.cpp b/core/libs/tests/PlayerInvitationDialog.cpp index 34fa83ede..4cbd37064 100644 --- a/core/libs/tests/PlayerInvitationDialog.cpp +++ b/core/libs/tests/PlayerInvitationDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h" #include +#include namespace { @@ -14,6 +15,12 @@ void expect(std::int32_t actual, std::int32_t expected) { int main() { using enum PlayerInvitationDialogStatus; + constexpr auto notInitialized = static_cast(0x80B80003u); + constexpr auto notFinished = static_cast(0x80B80005u); + constexpr auto argNull = static_cast(0x80B8000Du); + constexpr std::int32_t userCanceled = 1; + ScePlayerInvitationDialogResult result{}; + expect(scePlayerInvitationDialogGetResult(&result), notInitialized); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(None)); expect(scePlayerInvitationDialogClose(), 0); expect(scePlayerInvitationDialogTerminate(), 0); @@ -22,17 +29,26 @@ int main() { expect(scePlayerInvitationDialogInitialize(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Initialized)); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Initialized)); + expect(scePlayerInvitationDialogGetResult(nullptr), argNull); + expect(scePlayerInvitationDialogGetResult(&result), notFinished); ScePlayerInvitationDialogParam param{}; expect(scePlayerInvitationDialogOpen(¶m), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Running)); + expect(scePlayerInvitationDialogGetResult(&result), notFinished); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); + std::memset(&result, 0xFF, sizeof(result)); + expect(scePlayerInvitationDialogGetResult(&result), 0); + expect(result.errorCode, 0); + expect(result.result, userCanceled); + for (const std::uint8_t byte : result.reserved) expect(byte, 0); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); expect(scePlayerInvitationDialogOpen(nullptr), 0); expect(scePlayerInvitationDialogClose(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Finished)); expect(scePlayerInvitationDialogTerminate(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(None)); + expect(scePlayerInvitationDialogGetResult(&result), notInitialized); expect(scePlayerInvitationDialogOpen(nullptr), 0); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..46e321b56 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -93,7 +93,7 @@ Throughout the project, every function at every stage either **does exactly what - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - the error codes are unknown: a null or empty memory block, an app type other than 0x20000000 (PPSA14632) or 0x10000000 (PPSA26344) and a second initialization throw. The minimum memory size is unknown; the app type and the block are not used, as no endpoint is implemented -- AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures +- AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures - [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature - [fCWdlnmB1Ks](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown name, signature - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw @@ -166,7 +166,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. - [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented; half-border addressing, the border color table, gradients, LOD bias, offsets, LOD clamps, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights -- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. +- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped From d2d96ad5cc15641a7b5fd4f84ddb4c671c58fa20 Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:45:28 +0300 Subject: [PATCH 063/203] style(cmake): restore indent on test foreach in BUILD_TESTING block --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 41d948fd5..fb852062e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -508,7 +508,7 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) endforeach() if(BUILD_TESTING) -foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests}) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) From f617b40a458f817b98ade39f0294dc6b633f4042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 13:54:28 +0200 Subject: [PATCH 064/203] feat(tools): add a hardware oracle for RDNA instruction semantics Co-Authored-By: Claude --- docs/dev/HW_ORACLE.md | 64 ++++++++++++++ tools/hw-oracle/hw_oracle.py | 106 ++++++++++++++++++++++ tools/hw-oracle/oracle.c | 165 +++++++++++++++++++++++++++++++++++ tools/hw-oracle/template.s | 70 +++++++++++++++ 4 files changed, 405 insertions(+) create mode 100644 docs/dev/HW_ORACLE.md create mode 100644 tools/hw-oracle/hw_oracle.py create mode 100644 tools/hw-oracle/oracle.c create mode 100644 tools/hw-oracle/template.s diff --git a/docs/dev/HW_ORACLE.md b/docs/dev/HW_ORACLE.md new file mode 100644 index 000000000..917efdc65 --- /dev/null +++ b/docs/dev/HW_ORACLE.md @@ -0,0 +1,64 @@ +# Hardware oracle + +`tools/hw-oracle/` runs a few lines of RDNA assembly on a local AMD GPU, one input row per lane, and returns what the +hardware computed. Use it to settle instruction semantics that the ISA documentation, LLVM or Mesa leave open, then pin the +measured rows as expected values in `tests/execution/`. + +The PS5 GPU is gfx1013. Other RDNA2 GPUs (gfx103x) share the VALU, SALU, DS, MUBUF and MIMG encodings, apart from ops that +exist only on gfx10.1 (`v_mad_legacy_f32`, `v_mac_legacy_f32`, `v_mul_lo_i32`, `s_dcache_discard`, ...). RDNA3 also runs +the tool, but differs from gfx1013 in more places. Say which GPU you measured on in the PR. + +## Requirements + +- Linux with the `amdgpu` driver and read/write access to `/dev/kfd` (usually the `render` group). Root is not needed. +- The ROCm HSA runtime with its headers: `libhsa-runtime-dev` (Debian, Ubuntu), `rocm-runtime-devel` (Fedora), + `hsa-rocr` (Arch), or AMD's ROCm packages. A runtime under `ROCM_PATH` or `/opt/rocm` is used first. The rest of ROCm + is not needed. +- A C compiler, and `clang` + `ld.lld` with the AMDGPU target (`llc --version` lists `amdgcn`). If they aren't in + `PATH`, they are taken from `$ROCM_PATH/llvm/bin`. + +The first run builds `oracle.c` into `~/.cache/anyps5-hw-oracle/` (`HW_ORACLE_CACHE` overrides it). The target is read +from the GPU (`HW_ORACLE_TARGET` overrides it). + +## Kernel + +`template.s` wraps the body under test. Per lane: + +- `v4`-`v7` hold the row's 4 input dwords. `v10`-`v25` start at 0 and are stored as the result (16 dwords). +- Leave `v0` (lane id), `v1` (input offset), `v2` (output offset) and `s[4:7]` (input and output addresses) unchanged. +- 4 KiB of LDS. Rows run in workgroups of up to 1024 lanes, padded to whole waves with zero rows. +- Bytes passed as `extra` follow the rows of each dispatch, at `s[4:5] + 16 * rows`: buffer contents, texels, etc. + Build buffer and image descriptors in SGPRs from that address. + +The FLOAT_MODE of the dispatch is set with `ieee` (0/1), `denorm32` and `denorm16` (0-3, 3 = denormals kept). Float +atomics need `coarse`: on fine-grained system memory they do nothing. + +## Use + +```sh +cat > body.s <<'EOF' + v_add_f16 v10, v4, v5 +EOF +printf '0x3c00 0x3c00 0 0\n0x7e01 0x3c00 0 0\n' > rows.txt +python3 tools/hw-oracle/hw_oracle.py body.s rows.txt --outs 1 --ieee 0 +``` + +prints one line per row with the output dwords in hex. From Python: + +```python +import sys +sys.path.insert(0, "tools/hw-oracle") +from hw_oracle import run + +rows = [(a, b, 0, 0) for a in values for b in values] +results = run(body, rows, wave64=True, denorm32=0) +``` + +Usual workflow: run the instruction on rows that separate the candidate models (edge values, NaN payloads, rounding +ties), write a model that matches every row, implement it, and put measured rows in the execution test. + +## Limits + +Only compute kernels. Pixel-shader ops that read their inputs from LDS also work: `v_interp_*` reads the attribute +parameters from LDS at `M0`, so writing them there by hand is enough. Behaviour that depends on the graphics pipeline +(parameter cache, exports, rasterization, `SPI_*` state) can't be measured with it. diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py new file mode 100644 index 000000000..eee171f50 --- /dev/null +++ b/tools/hw-oracle/hw_oracle.py @@ -0,0 +1,106 @@ +import argparse +import os +import shutil +import struct +import subprocess +import sys +import tempfile +from functools import cache +from pathlib import Path + +HERE = Path(__file__).resolve().parent +CACHE = Path(os.environ.get("HW_ORACLE_CACHE") or Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "anyps5-hw-oracle") +ROWS_PER_DISPATCH = 1024 + + +def rocm_root(): + for root in (os.environ.get("ROCM_PATH"), "/opt/rocm"): + if root and (Path(root) / "include" / "hsa" / "hsa.h").exists(): + return Path(root) + return None + + +def tool(name): + found = shutil.which(name) + if found: + return found + root = rocm_root() + if root and (root / "llvm" / "bin" / name).exists(): + return str(root / "llvm" / "bin" / name) + sys.exit(f"{name} not found: install LLVM with the AMDGPU target or set ROCM_PATH") + + +def oracle(): + binary = CACHE / "oracle" + source = HERE / "oracle.c" + if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime: + return binary + CACHE.mkdir(parents=True, exist_ok=True) + command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(binary)] + root = rocm_root() + if root: + lib = root / "lib" + command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"] + subprocess.run(command + ["-lhsa-runtime64"], check=True) + return binary + + +@cache +def target(): + return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + + +def assemble(body, work, wave64, ieee, denorm32, denorm16): + text = (HERE / "template.s").read_text() + for key, value in (("@TARGET@", target()), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"), + ("@DENORM@", str(denorm32)), ("@DENORM16@", str(denorm16)), ("@IEEE@", str(ieee)), ("@BODY@", body)): + text = text.replace(key, value) + (work / "k.s").write_text(text) + subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True) + subprocess.run([tool("ld.lld"), "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True) + return work / "k.co" + + +def run(body, rows, extra=b"", wave64=False, ieee=1, denorm32=3, denorm16=3, coarse=False): + rows = [tuple(r) for r in rows] + wave = 64 if wave64 else 32 + padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) + env = dict(os.environ) + env.pop("COARSE", None) + if coarse: + env["COARSE"] = "1" + out = [] + with tempfile.TemporaryDirectory() as tmp: + work = Path(tmp) + code = assemble(body, work, wave64, ieee, denorm32, denorm16) + for start in range(0, len(padded), ROWS_PER_DISPATCH): + chunk = padded[start:start + ROWS_PER_DISPATCH] + (work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra) + subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env) + data = (work / "out.bin").read_bytes() + out += [struct.unpack_from("<16I", data, lane * 64) for lane in range(len(chunk))] + return out[:len(rows)] + + +def main(): + parser = argparse.ArgumentParser(description="Run an RDNA kernel body on the local AMD GPU, one input row per lane.") + parser.add_argument("body", type=Path, help="assembly inserted into template.s") + parser.add_argument("rows", type=Path, help="one row per line: 4 u32 values (decimal or 0x hex)") + parser.add_argument("--wave64", action="store_true") + parser.add_argument("--ieee", type=int, choices=(0, 1), default=1) + parser.add_argument("--denorm32", type=int, choices=range(4), default=3) + parser.add_argument("--denorm16", type=int, choices=range(4), default=3) + parser.add_argument("--coarse", action="store_true", help="input and output in coarse-grained GPU memory") + parser.add_argument("--extra", type=Path, help="bytes appended after the rows") + parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") + args = parser.parse_args() + rows = [tuple(int(v, 0) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] + 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"" + for result in run(args.body.read_text(), rows, extra, args.wave64, args.ieee, args.denorm32, args.denorm16, args.coarse): + print(" ".join(f"{v:08x}" for v in result[:args.outs])) + + +if __name__ == "__main__": + main() diff --git a/tools/hw-oracle/oracle.c b/tools/hw-oracle/oracle.c new file mode 100644 index 000000000..cb240017e --- /dev/null +++ b/tools/hw-oracle/oracle.c @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include + +#define CHECK(x) do { hsa_status_t s_ = (x); if (s_ != HSA_STATUS_SUCCESS && s_ != HSA_STATUS_INFO_BREAK) { const char* m_; hsa_status_string(s_, &m_); fprintf(stderr, "%s: %s\n", #x, m_); exit(1); } } while (0) + +static hsa_agent_t gpu; +static int haveGpu; +static hsa_amd_memory_pool_t kernargPool, finePool; +static int haveKernarg, haveFine; +static hsa_amd_memory_pool_t coarsePool; +static int haveCoarse; + +static hsa_status_t findCoarse(hsa_amd_memory_pool_t pool, void* data) { + hsa_amd_segment_t segment; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_SEGMENT, &segment); + if (segment != HSA_AMD_SEGMENT_GLOBAL) return HSA_STATUS_SUCCESS; + uint32_t flags; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags); + if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_COARSE_GRAINED) && !haveCoarse) { coarsePool = pool; haveCoarse = 1; } + return HSA_STATUS_SUCCESS; +} + +static hsa_status_t findGpu(hsa_agent_t agent, void* data) { + hsa_device_type_t type; + hsa_agent_get_info(agent, HSA_AGENT_INFO_DEVICE, &type); + if (type == HSA_DEVICE_TYPE_GPU) { gpu = agent; haveGpu = 1; return HSA_STATUS_INFO_BREAK; } + return HSA_STATUS_SUCCESS; +} + +static hsa_status_t findPools(hsa_amd_memory_pool_t pool, void* data) { + hsa_amd_segment_t segment; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_SEGMENT, &segment); + if (segment != HSA_AMD_SEGMENT_GLOBAL) return HSA_STATUS_SUCCESS; + uint32_t flags; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags); + if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_KERNARG_INIT) && !haveKernarg) { kernargPool = pool; haveKernarg = 1; } + if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_FINE_GRAINED) && !haveFine) { finePool = pool; haveFine = 1; } + return HSA_STATUS_SUCCESS; +} + +static hsa_status_t findCpu(hsa_agent_t agent, void* data) { + hsa_device_type_t type; + hsa_agent_get_info(agent, HSA_AGENT_INFO_DEVICE, &type); + if (type == HSA_DEVICE_TYPE_CPU) { hsa_amd_agent_iterate_memory_pools(agent, findPools, NULL); } + return HSA_STATUS_SUCCESS; +} + +static void* readFile(const char* path, size_t* size) { + FILE* f = fopen(path, "rb"); + if (!f) { perror(path); exit(1); } + fseek(f, 0, SEEK_END); *size = (size_t)ftell(f); fseek(f, 0, SEEK_SET); + void* data = malloc(*size); + if (fread(data, 1, *size, f) != *size) { perror(path); exit(1); } + fclose(f); + return data; +} + +int main(int argc, char** argv) { + if (argc == 2 && strcmp(argv[1], "--target") == 0) { + CHECK(hsa_init()); + CHECK(hsa_iterate_agents(findGpu, NULL)); + if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; } + char name[64] = {0}; + CHECK(hsa_agent_get_info(gpu, HSA_AGENT_INFO_NAME, name)); + printf("%s\n", name); + return 0; + } + if (argc != 6) { fprintf(stderr, "usage: oracle --target | oracle code.o threads in.bin out.bin out_dwords\n"); return 2; } + const uint32_t threads = (uint32_t)atoi(argv[2]); + const uint32_t outDwords = (uint32_t)atoi(argv[5]); + CHECK(hsa_init()); + CHECK(hsa_iterate_agents(findGpu, NULL)); + if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; } + CHECK(hsa_iterate_agents(findCpu, NULL)); + if (!haveKernarg || !haveFine) { fprintf(stderr, "no system memory pools\n"); return 1; } + + size_t codeSize; void* code = readFile(argv[1], &codeSize); + hsa_code_object_reader_t reader; + CHECK(hsa_code_object_reader_create_from_memory(code, codeSize, &reader)); + hsa_executable_t exe; + CHECK(hsa_executable_create_alt(HSA_PROFILE_FULL, HSA_DEFAULT_FLOAT_ROUNDING_MODE_DEFAULT, NULL, &exe)); + CHECK(hsa_executable_load_agent_code_object(exe, gpu, reader, NULL, NULL)); + CHECK(hsa_executable_freeze(exe, NULL)); + hsa_executable_symbol_t sym; + CHECK(hsa_executable_get_symbol_by_name(exe, "probe.kd", &gpu, &sym)); + uint64_t kernelObject; uint32_t kernargSize, groupSize, privateSize; + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_OBJECT, &kernelObject); + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_KERNARG_SEGMENT_SIZE, &kernargSize); + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_GROUP_SEGMENT_SIZE, &groupSize); + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_PRIVATE_SEGMENT_SIZE, &privateSize); + + size_t inSize; void* inHost = readFile(argv[3], &inSize); + void *in, *out, *args; + const size_t outSize = (size_t)threads * outDwords * 4u; + const int coarse = getenv("COARSE") != NULL; + if (coarse) { + CHECK(hsa_amd_agent_iterate_memory_pools(gpu, findCoarse, NULL)); + if (!haveCoarse) { fprintf(stderr, "no coarse-grained GPU pool\n"); return 1; } + } + void *inStage = NULL, *outStage = NULL; + CHECK(hsa_amd_memory_pool_allocate(coarse ? coarsePool : finePool, inSize, 0, &in)); + CHECK(hsa_amd_memory_pool_allocate(coarse ? coarsePool : finePool, outSize, 0, &out)); + if (coarse) { + CHECK(hsa_amd_memory_pool_allocate(finePool, inSize, 0, &inStage)); + CHECK(hsa_amd_memory_pool_allocate(finePool, outSize, 0, &outStage)); + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, inStage)); + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, outStage)); + } + CHECK(hsa_amd_memory_pool_allocate(kernargPool, 16, 0, &args)); + if (!coarse) { + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, in)); + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, out)); + } + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, args)); + if (coarse) { + memcpy(inStage, inHost, inSize); + memset(outStage, 0xee, outSize); + CHECK(hsa_memory_copy(in, inStage, inSize)); + CHECK(hsa_memory_copy(out, outStage, outSize)); + } else { + memcpy(in, inHost, inSize); + memset(out, 0xee, outSize); + } + ((void**)args)[0] = in; + ((void**)args)[1] = out; + + hsa_queue_t* queue; + CHECK(hsa_queue_create(gpu, 64, HSA_QUEUE_TYPE_SINGLE, NULL, NULL, UINT32_MAX, UINT32_MAX, &queue)); + hsa_signal_t done; + CHECK(hsa_signal_create(1, 0, NULL, &done)); + const uint64_t index = hsa_queue_add_write_index_relaxed(queue, 1); + hsa_kernel_dispatch_packet_t* packet = (hsa_kernel_dispatch_packet_t*)queue->base_address + (index & (queue->size - 1)); + memset((uint8_t*)packet + 4, 0, sizeof(*packet) - 4); + packet->setup = 1u << HSA_KERNEL_DISPATCH_PACKET_SETUP_DIMENSIONS; + packet->workgroup_size_x = (uint16_t)(threads < 1024 ? threads : 1024); + packet->workgroup_size_y = 1; + packet->workgroup_size_z = 1; + packet->grid_size_x = threads; + packet->grid_size_y = 1; + packet->grid_size_z = 1; + packet->kernel_object = kernelObject; + packet->kernarg_address = args; + packet->private_segment_size = privateSize; + packet->group_segment_size = groupSize; + packet->completion_signal = done; + const uint16_t header = (HSA_PACKET_TYPE_KERNEL_DISPATCH << HSA_PACKET_HEADER_TYPE) | + (HSA_FENCE_SCOPE_SYSTEM << HSA_PACKET_HEADER_ACQUIRE_FENCE_SCOPE) | (HSA_FENCE_SCOPE_SYSTEM << HSA_PACKET_HEADER_RELEASE_FENCE_SCOPE); + __atomic_store_n((uint32_t*)packet, header | ((uint32_t)packet->setup << 16), __ATOMIC_RELEASE); + hsa_signal_store_screlease(queue->doorbell_signal, (hsa_signal_value_t)index); + if (hsa_signal_wait_scacquire(done, HSA_SIGNAL_CONDITION_LT, 1, 5000000000ull, HSA_WAIT_STATE_BLOCKED) != 0) { + fprintf(stderr, "kernel timed out\n"); + return 1; + } + if (coarse) { + CHECK(hsa_memory_copy(outStage, out, outSize)); + out = outStage; + } + FILE* f = fopen(argv[4], "wb"); + if (!f || fwrite(out, 1, outSize, f) != outSize) { perror(argv[4]); return 1; } + fclose(f); + return 0; +} diff --git a/tools/hw-oracle/template.s b/tools/hw-oracle/template.s new file mode 100644 index 000000000..ad5426247 --- /dev/null +++ b/tools/hw-oracle/template.s @@ -0,0 +1,70 @@ +.amdgcn_target "amdgcn-amd-amdhsa--@TARGET@" +.text +.globl probe +.p2align 8 +.type probe,@function +probe: + s_load_dwordx4 s[4:7], s[0:1], 0x0 + v_lshlrev_b32 v1, 4, v0 + v_lshlrev_b32 v2, 6, v0 + s_waitcnt lgkmcnt(0) + global_load_dwordx4 v[4:7], v1, s[4:5] + s_waitcnt vmcnt(0) + v_mov_b32 v10, 0 + v_mov_b32 v11, 0 + v_mov_b32 v12, 0 + v_mov_b32 v13, 0 + v_mov_b32 v14, 0 + v_mov_b32 v15, 0 + v_mov_b32 v16, 0 + v_mov_b32 v17, 0 + v_mov_b32 v18, 0 + v_mov_b32 v19, 0 + v_mov_b32 v20, 0 + v_mov_b32 v21, 0 + v_mov_b32 v22, 0 + v_mov_b32 v23, 0 + v_mov_b32 v24, 0 + v_mov_b32 v25, 0 +@BODY@ + global_store_dwordx4 v2, v[10:13], s[6:7] + global_store_dwordx4 v2, v[14:17], s[6:7] offset:16 + global_store_dwordx4 v2, v[18:21], s[6:7] offset:32 + global_store_dwordx4 v2, v[22:25], s[6:7] offset:48 + s_endpgm +.Lend: +.size probe, .Lend-probe + +.rodata +.p2align 6 +.amdhsa_kernel probe + .amdhsa_user_sgpr_kernarg_segment_ptr 1 + .amdhsa_kernarg_size 16 + .amdhsa_group_segment_fixed_size 4096 + .amdhsa_next_free_vgpr 128 + .amdhsa_next_free_sgpr 48 + .amdhsa_wavefront_size32 @WAVE32@ + .amdhsa_float_denorm_mode_32 @DENORM@ + .amdhsa_float_denorm_mode_16_64 @DENORM16@ + .amdhsa_ieee_mode @IEEE@ +.end_amdhsa_kernel + +.amdgpu_metadata +--- +amdhsa.version: [ 1, 2 ] +amdhsa.kernels: + - .name: probe + .symbol: probe.kd + .kernarg_segment_size: 16 + .group_segment_fixed_size: 4096 + .private_segment_fixed_size: 0 + .kernarg_segment_align: 8 + .wavefront_size: @WAVESIZE@ + .sgpr_count: 48 + .vgpr_count: 64 + .max_flat_workgroup_size: 1024 + .args: + - { .size: 8, .offset: 0, .value_kind: global_buffer, .address_space: global } + - { .size: 8, .offset: 8, .value_kind: global_buffer, .address_space: global } +... +.end_amdgpu_metadata From e998b2dd66603afaa99e66f52df06e32ab6bbc45 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 6 Oct 2026 14:55:12 +0300 Subject: [PATCH 065/203] feat(agc): choose the Vulkan device by name with ANYPS5_GPU --- .../Execution/include/VulkanDevice.hpp | 2 + .../Execution/src/VulkanDevice.cpp | 28 +++++++++- .../tests/execution/DeviceSelection.cpp | 54 +++++++++++++++++++ docs/user/USAGE.md | 4 ++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 0064178b9..e2b3bfb55 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace AgcDriver { @@ -32,6 +33,7 @@ public: ~VulkanDevice(); VulkanDevice(const VulkanDevice&) = delete; VulkanDevice& operator=(const VulkanDevice&) = delete; + std::string DeviceName() const; ShaderRecompiler::SpirvTarget Target() const; ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const; // Distinguishes this device from every earlier one in the process (a memo keyed by the device diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5cadb337f..c4a4360ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -613,6 +615,20 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDevice selected = VK_NULL_HANDLE; std::uint32_t family = 0; int selectedRank = -1; + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (requestedName != nullptr && *requestedName == '\0') requestedName = nullptr; + const auto matchesRequest = [&](const char* deviceName) { + const std::string_view name(deviceName); + const std::string_view request(requestedName); + if (request.size() > name.size()) return false; + for (std::size_t start = 0; start + request.size() <= name.size(); ++start) { + std::size_t index = 0; + while (index < request.size() && std::tolower(static_cast(name[start + index])) == std::tolower(static_cast(request[index]))) ++index; + if (index == request.size()) return true; + } + return false; + }; + std::string candidateNames; const auto rankDeviceType = [](VkPhysicalDeviceType type) { switch (type) { case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; @@ -626,10 +642,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDeviceProperties properties{}; state->InstanceFunction("vkGetPhysicalDeviceProperties")(physical, &properties); APS5_LOG_OUT("Physical device candidate=%p name=%s api=0x%x type=%d", reinterpret_cast(physical), properties.deviceName, properties.apiVersion, static_cast(properties.deviceType)); + if (!candidateNames.empty()) candidateNames += ", "; + candidateNames += properties.deviceName; if (properties.apiVersion < VK_API_VERSION_1_1) { continue; } - const int rank = rankDeviceType(properties.deviceType); + if (requestedName != nullptr && !matchesRequest(properties.deviceName)) continue; + const int rank = requestedName != nullptr ? 4 : rankDeviceType(properties.deviceType); if (rank <= selectedRank) { continue; } @@ -663,6 +682,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u } } } + if (selected == VK_NULL_HANDLE && requestedName != nullptr) { + throw std::runtime_error(std::string("Vulkan: no usable device whose name contains ANYPS5_GPU=\"") + requestedName + "\"; devices: " + candidateNames); + } if (selected == VK_NULL_HANDLE) { throw std::runtime_error(window ? "Vulkan: no Vulkan 1.1 device with graphics, compute and swapchain presentation" : "Vulkan: no Vulkan 1.1 graphics and compute queue"); } @@ -2316,6 +2338,10 @@ ShaderRecompiler::SpirvTarget VulkanDevice::ComputeTarget(std::uint32_t waveSize return target; } +std::string VulkanDevice::DeviceName() const { + return state->properties.deviceName; +} + ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { const auto& limits = state->properties.limits; ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp new file mode 100644 index 000000000..b4579b321 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp @@ -0,0 +1,54 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +void SetRequest(const std::string& value) { +#ifdef _WIN32 + if (_putenv_s("ANYPS5_GPU", value.c_str()) != 0) throw std::runtime_error("cannot set ANYPS5_GPU"); +#else + if (setenv("ANYPS5_GPU", value.c_str(), 1) != 0) throw std::runtime_error("cannot set ANYPS5_GPU"); +#endif +} + +void Require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +} + +int main() { + try { + SetRequest(""); + const auto defaultDevice = OpenVulkanTestDevice(); + if (!defaultDevice) return VulkanTestSkipped; + const std::string name = defaultDevice->DeviceName(); + Require(!name.empty(), "the default device has no name"); + + std::string request = name; + for (auto& character : request) character = static_cast(std::toupper(static_cast(character))); + SetRequest(request); + const AgcDriver::VulkanDevice requested; + Require(requested.DeviceName() == name, "ANYPS5_GPU=" + request + " selected " + requested.DeviceName() + " instead of " + name); + + SetRequest("no such device 1d3d154f"); + try { + const AgcDriver::VulkanDevice missing; + throw std::runtime_error("ANYPS5_GPU naming no device selected " + missing.DeviceName()); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("ANYPS5_GPU=\"no such device 1d3d154f\"") != std::string::npos && message.find(name) != std::string::npos, "unexpected error: " + message); + } + std::puts("device selection tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 999714177..785c57379 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -89,6 +89,10 @@ Windows PowerShell: Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts. +### GPU selection + +The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. + ## Exit codes `0`: conversion succeeded. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. From 232719f9f8c005573640c48e96ce154632db5804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:06:52 +0200 Subject: [PATCH 066/203] feat(libs): implement sceWebBrowserDialogGetStatus Co-Authored-By: Claude --- .../prx/libSceWebBrowserDialog/CMakeLists.txt | 20 +++++++++++ .../prx/libSceWebBrowserDialog/Export.cpp | 4 +++ core/libs/tests/GuestWebBrowserDialog.cpp | 34 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestWebBrowserDialog.cpp diff --git a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt new file mode 100644 index 000000000..b73af180a --- /dev/null +++ b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceWebBrowserDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceWebBrowserDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceWebBrowserDialog) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_web_browser_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestWebBrowserDialog.cpp) + target_include_directories(guest_web_browser_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_web_browser_dialog_tests PRIVATE libSceWebBrowserDialog) + add_test(NAME guest_web_browser_dialog COMMAND guest_web_browser_dialog_tests) + if(WIN32) + set_tests_properties(guest_web_browser_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 879608486..be76145bf 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -46,6 +46,10 @@ int APS5_VABI sceWebBrowserDialogGetResult(void* result) { return 0; } +int APS5_VABI sceWebBrowserDialogGetStatus(void) { + return g_status.load(); +} + int APS5_VABI sceWebBrowserDialogOpen(const void* param) { const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp new file mode 100644 index 000000000..9e9f02fe0 --- /dev/null +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -0,0 +1,34 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceWebBrowserDialogInitialize(void); +int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogGetStatus(void); +int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +} + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); + std::uint8_t param[64] = {}; + Require(sceWebBrowserDialogOpen(param) == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); +} From 803f1b0e71d6ff3d7828ddb63e603b50a06b7215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:10:28 +0200 Subject: [PATCH 067/203] feat(videodec2): implement sceVideodec2GetAvcPictureInfo Co-Authored-By: Claude --- core/libs/prx/libSceVideodec2/Export.cpp | 4 ++++ core/libs/tests/GuestVideodec2.cpp | 9 +++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 055a5d509..28bea7762 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -428,4 +428,8 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A return 0; } +int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { + return sceVideodec2GetPictureInfo_nid_postfix(output, first, second); +} + } diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 5a7e8227a..89e1d2328 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -83,6 +83,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t, const InputData*, Fr int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t, FrameBuffer*, OutputInfo*); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t); int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo*, AvcPictureInfo*, void*); +int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); } namespace { @@ -272,6 +273,11 @@ void testFailures() { input.auSize = 2; checkThrows([&] { sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output); }); check(sceVideodec2Reset_nid_postfix(handle) == 0, "reset failed"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(nullptr, &avc, nullptr); }); + checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&output, nullptr, nullptr); }); + output.frameBuffer = buffer.data(); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && !avc.isValid, "unknown frame buffer reported a picture"); frame.frameBufferSize = 1; checkThrows([&] { for (const auto& unit : accessUnits(false)) { @@ -302,6 +308,9 @@ void testDecode(bool lengthPrefixed) { const auto unit = DisplayOrderUnits[pictures]; check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "picture timestamps out of display order"); check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "picture flags mismatch"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && avc.isValid, "avc picture info missing"); + check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == Height, "avc picture info mismatch"); check(hashNv12(buffer.data(), output.framePitch) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; return true; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..310c3da2a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -42,6 +42,7 @@ Throughout the project, every function at every stage either **does exactly what - [APR gather and scatter reads](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` - [AMM map, map direct and unmap](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved in the guest arena on first use, not the console's; maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. `sceAmprAmmGetVirtualAddressRanges` throws for a null output - [AMM remap, multimap and protection changes](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws +- [sceVideodec2GetAvcPictureInfo](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - [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 - [APR map begin and end](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded From 263cb6626c992fdb5bf5fe4e4b609d91dde481db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:16:07 +0200 Subject: [PATCH 068/203] feat(jpegenc): honor the encode restart interval Co-Authored-By: Claude --- core/Decoder/Jpeg/include/Decoder/Jpeg.hpp | 3 +- core/Decoder/Jpeg/src/Jpeg.cpp | 10 ++- core/Decoder/Jpeg/tests/Jpeg.cpp | 3 + core/libs/prx/libSceJpegEnc/Export.cpp | 9 ++- core/libs/tests/GuestJpegEnc.cpp | 78 +++++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 6 files changed, 99 insertions(+), 5 deletions(-) diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 60142fc36..13c1d8acc 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -28,7 +28,8 @@ enum class Sampling { }; std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444); + std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444, + std::uint32_t restartBlocks = 0, std::uint32_t restartRows = 0); std::optional
ParseHeader(std::span jpeg); diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index c8f1e1eca..d446841cc 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -49,7 +49,8 @@ int toSubsampling(Sampling sampling) { } // namespace std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality, Sampling sampling) { + std::uint32_t channels, int quality, Sampling sampling, std::uint32_t restartBlocks, + std::uint32_t restartRows) { if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { throw std::invalid_argument("Jpeg::Encode: unsupported image size"); } @@ -58,11 +59,18 @@ std::vector Encode(std::span pixels, std::uint if (pixels.size() < static_cast(width) * height * channels) { throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); } + if (restartBlocks > MAX_DIMENSION || restartRows > MAX_DIMENSION || (restartBlocks != 0 && restartRows != 0)) { + throw std::invalid_argument("Jpeg::Encode: invalid restart interval"); + } const int subsampling = toSubsampling(sampling); Handle handle; if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); } + if ((restartBlocks != 0 && tj3Set(handle, TJPARAM_RESTARTBLOCKS, static_cast(restartBlocks)) < 0) || + (restartRows != 0 && tj3Set(handle, TJPARAM_RESTARTROWS, static_cast(restartRows)) < 0)) { + throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); + } Buffer buffer; std::size_t size = 0; const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB; diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index d396a00f2..f5f9c309a 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -100,6 +100,9 @@ int main() { Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor); } Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast(3)); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 1, 1); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 0x10000, 0); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 0, 0x10000); })); Require(!Decoder::Jpeg::Decode({}).has_value()); const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index 2d4c66657..6de513f53 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -38,6 +38,8 @@ constexpr std::uint8_t SAMPLING_TYPE_FULL = 0; constexpr std::uint8_t SAMPLING_TYPE_422 = 1; constexpr std::uint8_t SAMPLING_TYPE_420 = 2; +constexpr std::int32_t RESTART_INTERVAL_PER_ROW = -1; + constexpr std::uint32_t MAX_IMAGE_DIMENSION = 0xFFFF; constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF; constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF; @@ -189,11 +191,14 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param const std::int32_t result = validateEncodeParam(param); if (result != 0) return result; if (param->encode_mode == ENCODE_MODE_MJPEG) throw std::runtime_error("sceJpegEncEncode: MJPEG encode mode is not implemented"); - if (param->restart_interval > 0) throw std::runtime_error("sceJpegEncEncode: restart interval is not implemented"); + if (param->restart_interval < RESTART_INTERVAL_PER_ROW) throw std::runtime_error("sceJpegEncEncode: unknown negative restart interval"); const std::uint32_t channels = param->pixel_format == PIXEL_FORMAT_Y8 ? 1 : 3; const std::vector pixels = toPackedPixels(*param, channels); - const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param)); + const std::uint32_t restartBlocks = param->restart_interval > 0 ? static_cast(param->restart_interval) : 0; + const std::uint32_t restartRows = param->restart_interval == RESTART_INTERVAL_PER_ROW ? 1 : 0; + const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param), + restartBlocks, restartRows); if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE; std::memcpy(param->jpeg, jpeg.data(), jpeg.size()); diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 5a49caa48..52437df7a 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -41,6 +41,30 @@ static std::pair ReadFrame(const unsigned char* jpeg, std::ui std::abort(); } +static std::pair ReadRestart(const unsigned char* jpeg, std::uint32_t size) { + int interval = 0; + std::size_t offset = 2; + while (true) { + Require(offset + 3 < size && jpeg[offset] == 0xFF); + const std::uint8_t marker = jpeg[offset + 1]; + const std::size_t length = (static_cast(jpeg[offset + 2]) << 8) | jpeg[offset + 3]; + Require(length >= 2 && offset + 2 + length <= size); + if (marker == 0xDD) { + Require(length == 4); + interval = (jpeg[offset + 4] << 8) | jpeg[offset + 5]; + } + offset += 2 + length; + if (marker == 0xDA) break; + } + int markers = 0; + for (; offset + 1 < size; ++offset) { + if (jpeg[offset] != 0xFF || jpeg[offset + 1] < 0xD0 || jpeg[offset + 1] > 0xD7) continue; + Require(jpeg[offset + 1] == 0xD0 + markers % 8); + ++markers; + } + return {interval, markers}; +} + static int AverageError(const std::vector& expected, const std::vector& actual) { long total = 0; for (std::size_t i = 0; i < expected.size(); ++i) total += expected[i] > actual[i] ? expected[i] - actual[i] : actual[i] - expected[i]; @@ -254,7 +278,59 @@ int main() { Require(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 0; }) == invalidParam); Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; }); })); - Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = 1; }); })); + Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = -2; }); })); + Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }); })); + + constexpr std::uint32_t wideWidth = 48; + constexpr std::uint32_t wideHeight = 32; + alignas(4) static unsigned char wide[wideWidth * wideHeight * 4]; + for (std::size_t i = 0; i < sizeof(wide); ++i) wide[i] = static_cast((i * 37) ^ (i / 192)); + static unsigned char wideGray[wideWidth * wideHeight]; + for (std::size_t i = 0; i < sizeof(wideGray); ++i) wideGray[i] = static_cast((i * 53) ^ (i / 48)); + static unsigned char restarted[8192]; + auto encodeWide = [&](std::uint8_t sampling, std::int32_t restart, bool grayscale) { + JpegEncEncodeParam p = ValidEncodeParam(); + p.image = grayscale ? wideGray : wide; + p.image_size = grayscale ? sizeof(wideGray) : sizeof(wide); + p.image_width = wideWidth; + p.image_height = wideHeight; + p.image_pitch = grayscale ? wideWidth : wideWidth * 4; + p.pixel_format = grayscale ? 11 : 0; + p.color_space = grayscale ? 2 : 1; + p.sampling_type = sampling; + p.jpeg = restarted; + p.jpeg_size = sizeof(restarted); + p.restart_interval = restart; + JpegEncOutputInfo out{}; + Require(sceJpegEncEncode(handle, &p, &out) == 0); + Require(IsJpeg(restarted, out.size) && out.height == wideHeight); + const auto decoded = Decoder::Jpeg::Decode({restarted, out.size}); + Require(decoded.has_value() && decoded->width == wideWidth && decoded->height == wideHeight); + return std::pair{ReadRestart(restarted, out.size), decoded->pixels}; + }; + + const auto plain420 = encodeWide(2, 0, false); + Require(plain420.first == std::pair{0, 0}); + const auto blocks420 = encodeWide(2, 2, false); + Require(blocks420.first == std::pair{2, 2}); + Require(blocks420.second == plain420.second); + const auto rows420 = encodeWide(2, -1, false); + Require(rows420.first == std::pair{3, 1}); + Require(rows420.second == plain420.second); + const auto plain422 = encodeWide(1, 0, false); + const auto rows422 = encodeWide(1, -1, false); + Require(rows422.first == std::pair{3, 3}); + Require(rows422.second == plain422.second); + const auto blocks422 = encodeWide(1, 5, false); + Require(blocks422.first == std::pair{5, 2}); + Require(blocks422.second == plain422.second); + const auto plainGray = encodeWide(0, 0, true); + const auto rowsGray = encodeWide(0, -1, true); + Require(rowsGray.first == std::pair{6, 3}); + Require(rowsGray.second == plainGray.second); + const auto blocksGray = encodeWide(0, 0xFFFF, true); + Require(blocksGray.first == std::pair{0xFFFF, 0}); + Require(blocksGray.second == plainGray.second); Require(sceJpegEncDelete(handle) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..63a82289e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -92,6 +92,7 @@ Throughout the project, every function at every stage either **does exactly what - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask +- [sceJpegEncEncode](../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - `restart_interval` follows SharpProspero's field doc (0 none, -1 every row of blocks, positive a block count); a positive value is taken as the JPEG DRI interval in MCUs and -1 as one restart per MCU row, which no reference confirms. Other negative values throw - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - the error codes are unknown: a null or empty memory block, an app type other than 0x20000000 (PPSA14632) or 0x10000000 (PPSA26344) and a second initialization throw. The minimum memory size is unknown; the app type and the block are not used, as no endpoint is implemented - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures - [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature From 1311e6e455d117e7c463d3ad9032a11c6dd90124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:19:40 +0200 Subject: [PATCH 069/203] feat(kernel): support getrusage RUSAGE_THREAD on Linux Co-Authored-By: Claude --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++++ .../libs/prx/libkernel/System/src/Process.cpp | 4 +- core/libs/tests/GuestGetrusage.cpp | 42 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestGetrusage.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 88b794890..172733c99 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -284,6 +284,16 @@ if(BUILD_TESTING) set_tests_properties(guest_available_cpumask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getrusage_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetrusage.cpp) + target_include_directories(guest_getrusage_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getrusage_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getrusage_tests) + add_test(NAME guest_getrusage COMMAND guest_getrusage_tests) + set_tests_properties(guest_getrusage PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getrusage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp) target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index a3838db51..cc9e1301f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -290,10 +290,8 @@ int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { usage->ru_nvcsw = 0; usage->ru_nivcsw = 0; #else - if (who == 1) - throw std::invalid_argument("getrusage: RUSAGE_THREAD is not supported on this platform"); struct rusage native{}; - if (::getrusage(RUSAGE_SELF, &native) != 0) + if (::getrusage(who == 0 ? RUSAGE_SELF : RUSAGE_THREAD, &native) != 0) throw std::system_error(errno, std::generic_category(), "getrusage: getrusage failed"); usage->ru_utime.tv_sec = static_cast(native.ru_utime.tv_sec); usage->ru_utime.tv_usec = static_cast(native.ru_utime.tv_usec); diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp new file mode 100644 index 000000000..a549808cf --- /dev/null +++ b/core/libs/tests/GuestGetrusage.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +struct Timeval { + std::int64_t tv_sec; + std::int64_t tv_usec; +}; +struct ResourceUsage { + Timeval ru_utime; + Timeval ru_stime; + std::int64_t rest[14]; +}; +extern "C" int APS5_VABI getrusage_nid_postfix(int who, ResourceUsage* usage); +static void Require(bool value) { if (!value) std::abort(); } +static std::int64_t CpuMicros(int who) { + ResourceUsage usage{}; + Require(getrusage_nid_postfix(who, &usage) == 0); + return (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; +} +static void SpinUntil(int who, std::int64_t micros) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + volatile std::uint64_t sink = 0; + while (CpuMicros(who) < micros) { + for (int i = 0; i < 100000; ++i) sink = sink + i; + Require(std::chrono::steady_clock::now() < deadline); + } +} +int main() { + SpinUntil(0, 400000); + const std::int64_t mainThread = CpuMicros(1); + Require(mainThread >= 200000); + std::thread worker([] { + const std::int64_t fresh = CpuMicros(1); + Require(fresh < 100000); + SpinUntil(1, fresh + 100000); + Require(CpuMicros(0) >= CpuMicros(1) + 300000); + }); + worker.join(); + Require(CpuMicros(0) >= 500000); +} From fe557fb0e231d556cec642cf2b27677e7a5b93e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:23:25 +0200 Subject: [PATCH 070/203] feat(kernel): lazily initialize the adaptive static mutex initializer Co-Authored-By: Claude --- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 18 ++++++++++++------ core/libs/tests/GuestKernelErrors.cpp | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 5bd606db6..976569adb 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -20,8 +20,12 @@ PthreadMutex destroyedMutex() { return reinterpret_cast(std::uintptr_t{2}); } +PthreadMutex adaptiveInitializer() { + return reinterpret_cast(std::uintptr_t{1}); +} + bool isInitializedMutex(PthreadMutex mutex) { - return mutex && mutex != destroyedMutex() && reinterpret_cast(mutex) != 1; + return mutex && mutex != destroyedMutex() && mutex != adaptiveInitializer(); } PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { @@ -34,13 +38,13 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { const auto current = slot.load(std::memory_order_acquire); if (current == destroyedMutex()) throw std::runtime_error("Mutex has been destroyed"); - if (reinterpret_cast(current) == 1) - throw std::runtime_error("Adaptive mutex initializer is unsupported"); - if (current) + if (current && current != adaptiveInitializer()) return current; if (!initialize) throw std::runtime_error("Mutex is not initialized"); auto* created = new PthreadMutexPrivate(); + if (current == adaptiveInitializer()) + created->_type = MutexType::Adaptive; slot.store(created, std::memory_order_release); return created; } @@ -144,8 +148,10 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { std::lock_guard lock(initializationMutex); if (*mutex == destroyedMutex()) throw std::runtime_error("Mutex has already been destroyed"); - if (reinterpret_cast(*mutex) == 1) - throw std::runtime_error("Adaptive mutex initializer is unsupported"); + if (*mutex == adaptiveInitializer()) { + std::atomic_ref(*mutex).store(destroyedMutex(), std::memory_order_release); + return 0; + } if (*mutex && (*mutex)->_owner.load(std::memory_order_acquire) != std::thread::id{}) throw std::runtime_error("Cannot destroy a locked mutex"); delete *mutex; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index d68975fdd..e29493539 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -28,6 +28,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); } static constexpr int SCE_OK = 0; @@ -36,6 +37,7 @@ static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000B); static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static constexpr int MUTEX_TYPE_ERRORCHECK = 1; static constexpr int PRIO_NONE = 0; @@ -108,6 +110,21 @@ int main() { Require(scePthreadMutexUnlock(&adaptive) == SCE_OK); Require(scePthreadMutexDestroy(&adaptive) == SCE_OK); + PthreadMutex adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexLock(&adaptiveStatic) == SCE_OK); + Require(adaptiveStatic != reinterpret_cast(std::uintptr_t{1})); + Require(scePthreadMutexLock(&adaptiveStatic) == SCE_KERNEL_ERROR_EDEADLK); + Require(scePthreadMutexTrylock(&adaptiveStatic) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexDestroy(&adaptiveStatic) == SCE_OK); + adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexTrylock(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexTrylock(&adaptiveStatic) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexDestroy(&adaptiveStatic) == SCE_OK); + adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexDestroy(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexattrInit(&attr) == SCE_OK); PthreadMutex defaulted = nullptr; Require(scePthreadMutexInit(&defaulted, &attr, nullptr) == SCE_OK); From ca6557aed17e6b94e60369ede9e7923eb11a5c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:27:48 +0200 Subject: [PATCH 071/203] feat(kernel): reject invalid sceKernelBatchMap2 entries with EINVAL Co-Authored-By: Claude --- .../prx/libkernel/DirectMemory/Export.cpp | 6 ++-- core/libs/tests/GuestMemory.cpp | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 096519100..75c39ff73 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -403,6 +403,10 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, int result = 0; for (; processed < num_entries; ++processed) { auto& entry = entries[processed]; + if (entry.length == 0 || entry.operation < OpMapDirect || entry.operation > OpTypeProtect) { + result = SCE_KERNEL_ERROR_EINVAL; + break; + } switch (entry.operation) { case OpMapDirect: result = DoMapDirect(&entry.start, entry.length, static_cast(entry.protection), flags, static_cast(entry.offset), 0); @@ -417,8 +421,6 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, case OpMapFlexible: result = _mapFlexible(&entry.start, entry.length, static_cast(entry.protection), flags); break; - default: - throw std::invalid_argument("sceKernelBatchMap2: unsupported operation " + std::to_string(entry.operation)); } if (result != 0) break; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 4485bf100..27dd441ab 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -61,6 +61,7 @@ int APS5_VABI sceKernelAioWaitRequest(std::int32_t, std::int32_t*, std::uint32_t int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*); int APS5_VABI sceKernelMlock_nid_postfix(void*, std::uint64_t); int APS5_VABI sceKernelGetDirectMemoryType(std::int64_t, int*, std::int64_t*, std::int64_t*); +int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -123,6 +124,38 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckBatchMapStopsAtInvalidEntry() { + constexpr std::size_t page = 0x4000; + constexpr int mapFlexible = 3; + constexpr int unmap = 1; + constexpr int protect = 2; + const auto flexible = [&] { return KernelBatchMapEntry{nullptr, 0, page, 3, 0, 0, mapFlexible}; }; + for (const std::int32_t operation : {5, 6, -1, std::numeric_limits::max()}) { + KernelBatchMapEntry entries[3] = {flexible(), flexible(), flexible()}; + entries[1].operation = operation; + int processed = -1; + Require(sceKernelBatchMap2(entries, 3, &processed, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(processed == 1); + Require(entries[0].start != nullptr && entries[1].start == nullptr && entries[2].start == nullptr); + Require(sceKernelMunmap(entries[0].start, page) == 0); + } + KernelBatchMapEntry entries[3] = {flexible(), flexible(), flexible()}; + entries[1].operation = protect; + entries[1].length = 0; + int processed = -1; + Require(sceKernelBatchMap2(entries, 3, &processed, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(processed == 1 && entries[0].start != nullptr); + Require(entries[1].start == nullptr && entries[2].start == nullptr); + KernelBatchMapEntry unmaps[2] = {entries[0], entries[0]}; + unmaps[0].operation = unmap; + unmaps[1].operation = unmap; + unmaps[1].length = 0; + processed = -1; + Require(sceKernelBatchMap2(unmaps, 2, &processed, 0) == SCE_KERNEL_ERROR_EINVAL && processed == 1); + Require(sceKernelBatchMap2(entries, 1, &processed, 0) == 0 && processed == 1); + Require(sceKernelMunmap(entries[0].start, page) == 0); +} + static void CheckCheckedReleaseDirectMemory() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -799,6 +832,7 @@ static void CheckDirectMemoryWriteWatch() { int main() { CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckDirectMemoryFollowsPhysicalPages(); CheckReleaseDirectMemoryClearsMappings(); From 109f120595700818b711366c6b3a331519edf3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:32:47 +0200 Subject: [PATCH 072/203] feat(kernel): apply the memory type in sceKernelMtypeprotect Co-Authored-By: Claude --- .../libkernel/DirectMemory/DirectMemory.cpp | 27 ++++++++++ .../libkernel/DirectMemory/DirectMemory.hpp | 2 + .../prx/libkernel/DirectMemory/Export.cpp | 7 +-- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 20 ++++++++ core/libs/tests/GuestMemory.cpp | 49 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 103 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 6a8b32f5f..508179717 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -649,6 +649,33 @@ int DoMprotect(const void* addr, size_t len, int prot) { return 0; } +int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { + const int result = DoMprotect(addr, len, prot); + const auto address = reinterpret_cast(addr); + constexpr auto pageMask = static_cast(PS5_PAGE_SIZE - 1); + const auto first = address & ~pageMask; + const auto end = (address + len + pageMask) & ~pageMask; + std::vector> physical; + { + std::lock_guard lock(g_directLock); + auto it = g_directMappings.lower_bound(first); + if (it != g_directMappings.begin() && std::prev(it)->second.end > first) --it; + while (it != g_directMappings.end() && it->first < end) { + const auto base = it->first; + const auto mapping = it->second; + it = g_directMappings.erase(it); + if (base < first) g_directMappings.emplace(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); + const auto low = std::max(base, first); + const auto high = std::min(mapping.end, end); + g_directMappings.emplace(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); + physical.emplace_back(mapping.phys + low - base, high - low); + if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + } + } + for (const auto& [phys, bytes] : physical) DirectMemoryRetype(static_cast(phys), bytes, type); + return result; +} + int DoMunmap(void* addr, size_t len) { Trace("unmap %p+0x%zx", addr, len); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index c1723243b..e2310818d 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -16,10 +16,12 @@ void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); void ForgetDirectMemory(int64_t start, size_t len); bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags); int DoMprotect(const void* addr, size_t len, int prot); +int DoMtypeprotect(const void* addr, size_t len, int type, int prot); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment); bool GuestProtection(uintptr_t addr, int* prot); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 096519100..04051f661 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -309,8 +309,7 @@ int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { } int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int prot) { - (void)type; - return DoMprotect(addr, len, prot); + return DoMtypeprotect(addr, len, type, prot); } int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, int* prot) { @@ -411,9 +410,11 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, result = sceKernelMunmap(reinterpret_cast(entry.start), entry.length); break; case OpProtect: - case OpTypeProtect: result = DoMprotect(entry.start, entry.length, static_cast(entry.protection)); break; + case OpTypeProtect: + result = DoMtypeprotect(entry.start, entry.length, static_cast(entry.type), static_cast(entry.protection)); + break; case OpMapFlexible: result = _mapFlexible(&entry.start, entry.length, static_cast(entry.protection), flags); break; diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index b1e04c262..645098b71 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include #include #include @@ -56,6 +57,21 @@ struct PhysicalMemoryPool { return true; } + void Retype(uint64_t start, size_t len, int memoryType) { + std::lock_guard lock(_mutex); + const uint64_t end = start + len; + auto it = _blocks.upper_bound(start); + if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; + while (it != _blocks.end() && it->first < end) { + const uint64_t blockStart = it->first; + const Block block = it->second; + it = _blocks.erase(it); + if (blockStart < start) _blocks[blockStart] = {start, block.type}; + _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; + if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + } + } + size_t FreeRun(uint64_t offset, uint64_t limit) { std::lock_guard lock(_mutex); uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); @@ -125,6 +141,10 @@ bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* en return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } +void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { + PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); +} + size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { return PhysicalMemoryPool::Instance().FreeRun(offset, limit); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 4485bf100..d98a0453d 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -42,6 +42,8 @@ int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); +int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); @@ -242,6 +244,52 @@ static void CheckGetDirectMemoryType() { Require(sceKernelGetDirectMemoryType(second, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); } +static void CheckMtypeprotect() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + Require(sceKernelMtypeprotect(bytes + page + 1, 1, 3, 1) == 0); + const auto check = [&](std::size_t index, int expectedType, int expectedProtection) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(bytes + page * index, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && info.memory_type == expectedType && info.protection == expectedProtection); + Require(info.start == reinterpret_cast(bytes + page * index) && info.end == info.start + page); + Require(info.offset == static_cast(phys) + page * index); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(phys + static_cast(page * index), &type, &start, &end) == 0); + Require(type == expectedType && start == phys + static_cast(page * index) && end == start + static_cast(page)); + }; + check(0, 0, 3); + check(1, 3, 1); + check(2, 0, 3); + KernelBatchMapEntry entry{}; + entry.start = bytes + page * 2; + entry.length = page; + entry.protection = 3; + entry.type = 5; + entry.operation = 4; + int processed = -1; + Require(sceKernelBatchMap2(&entry, 1, &processed, 0) == 0 && processed == 1); + check(0, 0, 3); + check(1, 3, 1); + check(2, 5, 3); + Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); + VirtualQueryInfo whole{}; + Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); + Require(whole.memory_type == 2 && whole.protection == 3); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(phys + static_cast(page * 2), &type, &start, &end) == 0 && type == 2); + Require(sceKernelMunmap(mapped, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +} + static void CheckFixedVirtualReservation() { constexpr std::size_t page = 0x4000; void* probe = nullptr; @@ -807,6 +855,7 @@ int main() { CheckMlock(); CheckSharedDirectMemoryLifecycle(); CheckGetDirectMemoryType(); + CheckMtypeprotect(); CheckHeapAfterMappingReuse(); #ifdef _WIN32 CheckNoOverwriteRejectsHostOccupiedMapping(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..67f7c9e39 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -70,6 +70,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted +- [sceKernelMtypeprotect](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature From 70864694d3881dbb1fbb102de7a98b5e7124bc4e Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 6 Oct 2026 14:36:20 +0200 Subject: [PATCH 073/203] fix(audioout): accept a 3D latency of 1 in sceAudioOut2Set3DLatency PPSA14632 calls sceAudioOut2Set3DLatency(0xFF, 1) during its audio setup (eboot 0x5613e2c) and fails the setup on a nonzero return. Only latency 2 from PPSA26344 was accepted, so the call threw. Latency 1 and 2 are now accepted for the system user; 0, 3 and other users still throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp | 5 +++-- core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp | 3 ++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index f658ddf10..cc3fdc2ea 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -3,7 +3,8 @@ #include "prx/libc/include/General.hpp" static constexpr int USER_ID_SYSTEM = 0xFF; -static constexpr std::uint32_t SUPPORTED_3D_LATENCY = 2; +static constexpr std::uint32_t MIN_SUPPORTED_3D_LATENCY = 1; +static constexpr std::uint32_t MAX_SUPPORTED_3D_LATENCY = 2; extern "C" { @@ -12,7 +13,7 @@ int APS5_VABI sceAudioOut2Initialize(void) { } int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t latency) { - if (userId != USER_ID_SYSTEM || latency != SUPPORTED_3D_LATENCY) NotImplemented_nid_no_patch(__func__); + if (userId != USER_ID_SYSTEM || latency < MIN_SUPPORTED_3D_LATENCY || latency > MAX_SUPPORTED_3D_LATENCY) NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 7f3404f2a..c0882d45c 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -35,7 +35,8 @@ constexpr int user = 0x10000000; void TestSet3DLatency() { Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "latency 2 for the system user must be accepted"); Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "latency 2 must be accepted again"); - Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 1); }), "latency 1 must throw"); + Require(sceAudioOut2Set3DLatency(systemUser, 1) == 0, "latency 1 for the system user must be accepted"); + Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 0); }), "latency 0 must throw"); Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 3); }), "latency 3 must throw"); Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(user, 2); }), "a user other than the system user must throw"); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..8c4409083 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -81,7 +81,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature -- [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the only known call sets just edi and esi; only `(0xFF, 2)` is accepted +- [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function From 5784515e13b1f49638d831cd3b04918c9a0ba876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:39:02 +0200 Subject: [PATCH 074/203] fix(libs): use the referenced NGS2 handle and session signaling argument error codes Co-Authored-By: Claude --- core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp | 4 ++-- core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp | 4 ++-- core/libs/prx/libSceNpSessionSignaling/Export.cpp | 2 +- core/libs/tests/GuestNpSessionSignaling.cpp | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index e7d5fd04c..f609b6fcc 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,8 +9,8 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); -static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A8201); -static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A8202); +static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A0230); +static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A0261); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 5250c7eee..85b306a22 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -37,8 +37,8 @@ static void TestErrorsAndInfo() { Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SAMPLER, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Ngs2SystemInfo info{}; - Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); - Require(sceNgs2RackDestroy(0x1234, nullptr) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == static_cast(0x804A0230u)); + Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A0261u)); const auto system = CreateSystem(); CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index eb3ee27e0..996613a42 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -5,7 +5,7 @@ #include // Peer-to-peer signaling needs the network: contexts exist, but sessions never activate. -static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80552D02); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80553303); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast(0x80552D06); static std::atomic g_nextContext{1}; diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 10ec8b222..834f15fb4 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -14,7 +14,7 @@ int APS5_VABI sceNpSessionSignalingTerminate(void); namespace { -constexpr int InvalidArgument = static_cast(0x80552D02u); +constexpr int InvalidArgument = static_cast(0x80553303u); constexpr int Unavailable = static_cast(0x80552D06u); void Require(bool condition, const char* message) { From b6176624aa2ff90c56b6ae29de541468f31e3b98 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 6 Oct 2026 14:42:51 +0200 Subject: [PATCH 075/203] fix(pad): return INVALID_HANDLE from scePadReadState for an unknown handle PPSA14632 calls scePadReadState (eboot 0x4593a8) for the open pad and then for a second pad slot it never opened, whose handle is 0. The title skips only negative handles and ignores the result, but scePadReadState threw for any handle other than the open one. It now returns PAD_ERROR_INVALID_HANDLE (0x80920003), as the other pad functions do and as shadPS4's scePadRead does for an unknown handle; scePadRead passes it on. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePad/Export.cpp | 2 +- core/libs/tests/GuestPad.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index adc42a6b7..92848cf95 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -150,7 +150,7 @@ int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { } int APS5_VABI scePadReadState(int handle, PadData* data) { - if (handle != 1) APS5_INVALID_ARG_EX; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr) APS5_INVALID_ARG_EX; *data = Pad::ReadState(); diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 522d99171..866c44ba7 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -12,6 +12,8 @@ int APS5_VABI scePadClose_nid_postfix(int); int APS5_VABI scePadGetHandle(int, int, int); int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool); int APS5_VABI scePadInit_nid_postfix(void); +int APS5_VABI scePadRead_nid_postfix(int, PadData*, int); +int APS5_VABI scePadReadState(int, PadData*); int APS5_VABI scePadSetTiltCorrectionState(int, bool); int APS5_VABI scePadResetOrientation(int); int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); @@ -57,6 +59,14 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckReadStateHandle(int handle) { + PadData data{}; + Require(scePadReadState(0, &data) == PAD_ERROR_INVALID_HANDLE); + Require(scePadReadState(handle + 1, &data) == PAD_ERROR_INVALID_HANDLE); + Require(scePadRead_nid_postfix(0, &data, 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadReadState(handle, &data) == PAD_OK); +} + int main() { constexpr int noHandle = static_cast(0x80920008); constexpr int user = 0x10000000; @@ -71,6 +81,7 @@ int main() { Require(scePadGetHandle(user, 2, 0) == handle); CheckTiltCorrection(handle); CheckTouchContact(); + CheckReadStateHandle(handle); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); From 5e18e84f333eb8dd9b5f90851ad702c9e72d8085 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:45:10 +0200 Subject: [PATCH 076/203] feat(ajm): implement sceAjmBatchJobControl sceAjmBatchJobControl threw. It now appends a control job that keeps the sideband input pointer and runs at sceAjmBatchStart: RESET (bit 13) clears the context as sceAjmBatchJobClearContext does, INITIALIZE (bit 14) initializes the codec, and SIDEBAND_GAPLESS_DECODE (bit 45) overwrites the gapless total and skip counts, in this order. The sideband input holds the gapless decode first, then the codec's initialize parameters: 8 bytes for ATRAC9, 12 for Opus, none for MP3. The output is the 8-byte result. Other flags, INITIALIZE for other codecs, and other sizes throw. SIDEBAND_GAPLESS_DECODE without RESET throws too: RPCSX and shadPS4 keep zero counts there. The bits and the order follow shadPS4 (PS4), the overwrite with RESET follows RPCSX, and the sizes follow the one title seen. --- core/libs/prx/libSceAjm.native/Export.cpp | 12 -- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 63 ++++++++- core/libs/tests/GuestAjm.cpp | 155 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 4 + 4 files changed, 216 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 62c0bc44e..4c3bbc33a 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -14,18 +14,6 @@ static void AjmStub(const char* name) { extern "C" { -int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { - (void)info; - (void)instance; - (void)flags; - (void)sideband_input; - (void)sideband_input_size; - (void)sideband_output; - (void)sideband_output_size; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 9b5542631..93088b0f7 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -56,6 +56,8 @@ bool TraceEnabled() { constexpr std::uint64_t RUN_GET_CODEC_INFO = 1ull << 11; constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12; +constexpr std::uint64_t CONTROL_RESET = 1ull << 13; +constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; constexpr std::uint64_t SIDEBAND_FORMAT = 1ull << 46; constexpr std::uint64_t SIDEBAND_STREAM = 1ull << 47; @@ -139,6 +141,7 @@ enum class JobKind : std::uint32_t { SetGaplessDecode = 3, Run = 4, GetStatistics = 5, + Control = 6, }; struct JobHeader { @@ -844,6 +847,43 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); } +void ClearContext(Instance& instance) { + instance.totalDecodedSamples = 0; + instance.gapless.skippedSamples = 0; + if (instance.decoder && instance.initialized) ResetDecoder(instance); + if (instance.mp3) avcodec_flush_buffers(instance.mp3); + if (instance.opus) avcodec_flush_buffers(instance.opus); + instance.opusPending.clear(); +} + +std::size_t ControlInitializeSize(std::uint32_t codec) { + switch (codec) { + case CODEC_MP3: return 0; + case CODEC_AT9: return 8; + case CODEC_OPUS: return 12; + default: NotImplemented_nid_no_patch("sceAjmBatchJobControl (INITIALIZE for a codec other than MP3, ATRAC9 and Opus)"); return 0; + } +} + +void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) { + const auto* input = job.inputCount ? static_cast(inputs[0].ptr) : nullptr; + const std::size_t inputSize = job.inputCount ? inputs[0].size : 0; + const std::size_t gaplessSize = (job.flags & SIDEBAND_GAPLESS_DECODE) ? sizeof(SidebandGaplessDecode) : 0; + const std::size_t initializeSize = (job.flags & CONTROL_INITIALIZE) ? ControlInitializeSize(instance.codec) : 0; + if (inputSize != gaplessSize + initializeSize) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband input size other than the gapless decode and the codec's initialize parameters)"); + std::int32_t result = 0; + if (job.flags & CONTROL_RESET) ClearContext(instance); + if (job.flags & CONTROL_INITIALIZE) result = InitializeInstance(instance, input + gaplessSize, initializeSize); + if (gaplessSize) { + SidebandGaplessDecode gapless{}; + std::memcpy(&gapless, input, sizeof(gapless)); + instance.gapless.totalSamples = gapless.totalSamples; + instance.gapless.skipSamples = gapless.skipSamples; + } + AJM_TRACE("[ajm] instance %u control flags 0x%llx: %zu sideband input bytes -> result 0x%x, gapless total %u skip %u skipped %u\n", job.instance, static_cast(job.flags), inputSize, static_cast(result), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); + WriteResult(job.sideband, job.sidebandSize, result); +} + void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -866,12 +906,7 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out } case JobKind::ClearContext: AJM_TRACE("[ajm] instance %u clear context (sideband %llu bytes)\n", job.instance, static_cast(job.sidebandSize)); - instance->totalDecodedSamples = 0; - instance->gapless.skippedSamples = 0; - if (instance->decoder && instance->initialized) ResetDecoder(*instance); - if (instance->mp3) avcodec_flush_buffers(instance->mp3); - if (instance->opus) avcodec_flush_buffers(instance->opus); - instance->opusPending.clear(); + ClearContext(*instance); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::SetGaplessDecode: { @@ -884,6 +919,9 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out WriteResult(job.sideband, job.sidebandSize, 0); break; } + case JobKind::Control: + Control(*instance, job, inputs); + break; case JobKind::Run: if (!instance->initialized) { AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance); @@ -1029,6 +1067,19 @@ int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instan return Append(info, header, nullptr, nullptr); } +int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { + constexpr std::uint64_t supported = CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; + if (flags == 0 || (flags & ~supported) != 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (flags other than a combination of RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); + if ((flags & SIDEBAND_GAPLESS_DECODE) != 0 && (flags & CONTROL_RESET) == 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (SIDEBAND_GAPLESS_DECODE without RESET)"); + if (sideband_output_size != sizeof(SidebandResult)) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband output other than the 8-byte result)"); + if (sideband_input_size != 0 && !sideband_input) NotImplemented_nid_no_patch("sceAjmBatchJobControl (null sideband input)"); + auto header = MakeHeader(JobKind::Control, instance, sideband_output, sideband_output_size); + header.flags = flags; + header.inputCount = sideband_input_size != 0 ? 1 : 0; + const AjmBuffer input{const_cast(sideband_input), sideband_input_size}; + return Append(info, header, &input, nullptr); +} + int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size) { auto header = MakeHeader(JobKind::Run, instance, sideband_output, sideband_output_size); header.flags = flags; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 0e9bb2483..72d6bb691 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -21,6 +21,7 @@ int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo*, std::uint32_t, const void*, st int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*, std::size_t, void*); int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t, void*, std::size_t); int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo*, std::uint32_t, const void*, int, void*); +int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t); int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo*, std::uint32_t, void*, std::size_t); int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); @@ -464,6 +465,157 @@ void TestBatchCancel(std::uint32_t context) { Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); } +constexpr std::uint64_t CONTROL_RESET = 1ull << 13; +constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; +constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; +constexpr std::uint64_t CONTROL_START = CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; + +struct At9Control { + GaplessDecode gapless; + std::uint8_t config[4]; + std::uint32_t reserved; +}; + +struct OpusControl { + GaplessDecode gapless; + std::uint32_t channels; + std::uint32_t sampleRate; + std::uint32_t third; +}; + +void RunControl(std::uint32_t context, std::uint32_t instance, std::uint64_t flags, const void* input, std::size_t inputSize, std::int32_t* result) { + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, flags, input, inputSize, result, 2 * sizeof(std::int32_t)) == 0); + Submit(context, info); +} + +bool ControlThrows(std::uint32_t instance, std::uint64_t flags, const void* input, std::size_t inputSize, std::size_t outputSize) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[4] = {}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + try { + static_cast(sceAjmBatchJobControl(&info, instance, flags, input, inputSize, result, outputSize)); + } catch (const std::runtime_error&) { + return info.offset == 0; + } + return false; +} + +bool ControlRefused(std::uint32_t context, std::uint32_t instance, std::uint64_t flags, const void* input, std::size_t inputSize) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, flags, input, inputSize, result, sizeof(result)) == 0); + return Refused(context, info); +} + +void TestControlAt9(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector batch(4096); + const At9Control start{{2000, 100, 0}, {0xFE, 0x72, 0x1F, 0xF0}, 0xDEADBEEFu}; + std::int32_t result[4] = {-1, -1, -1, -1}; + At9CodecInfoSideband codec{-1, -1, 0, 0, 0, 0}; + GaplessSideband gapless{-1, -1, 0, 0, 0xffff}; + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, CONTROL_START, &start, sizeof(start), result, 2 * sizeof(std::int32_t)) == 0); + Require(sceAjmBatchJobGetCodecInfo(&info, instance, &codec, sizeof(codec)) == 0); + Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &gapless) == 0); + Submit(context, info); + Require(result[0] == 0 && result[1] == 0 && result[2] == -1 && result[3] == -1); + Require(codec.result == 0 && codec.superframeSize == 1024 && codec.framesInSuperframe == 4 && codec.nextFrameSize == 1024 && codec.frameSamples == 256); + Require(gapless.result == 0 && gapless.totalSamples == 2000 && gapless.skipSamples == 100 && gapless.skippedSamples == 0); + + At9Control broken = start; + broken.config[0] = 0xFD; + At9CodecInfoSideband uninitialized{-1, -1, 0, 0, 0, 0}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, CONTROL_START, &broken, sizeof(broken), result, 2 * sizeof(std::int32_t)) == 0); + Require(sceAjmBatchJobGetCodecInfo(&info, instance, &uninitialized, sizeof(uninitialized)) == 0); + Submit(context, info); + Require(result[0] == 4 && uninitialized.result == 1); + + result[0] = -1; + RunControl(context, instance, CONTROL_INITIALIZE, start.config, sizeof(start) - sizeof(start.gapless), result); + Require(result[0] == 0); + + Require(ControlRefused(context, instance, CONTROL_INITIALIZE, &start, sizeof(start))); + Require(ControlRefused(context, instance, CONTROL_RESET | SIDEBAND_GAPLESS_DECODE, &start, sizeof(start))); + Require(ControlThrows(instance, CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE, &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START | (1ull << 15), &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START | (1ull << 46), &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START | (1ull << 12), &start, sizeof(start), 8)); + Require(ControlThrows(instance, 0, nullptr, 0, 8)); + Require(ControlThrows(instance, CONTROL_START, &start, sizeof(start), 16)); + Require(ControlThrows(instance, CONTROL_START, nullptr, sizeof(start), 8)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + result[0] = -1; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 4); + + const std::uint32_t aac[4] = {0, 0, 1, 0}; + Require(sceAjmInstanceCreate(context, 2, 0, &instance) == 0); + Require(ControlRefused(context, instance, CONTROL_START, aac, sizeof(aac))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestControlMp3(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); + std::vector pcm(1152); + const std::size_t pcmBytes = pcm.size() * sizeof(std::int16_t); + DecodeSideband decoded{}; + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + RunDecode(context, instance, MP3_MONO + 96, sizeof(MP3_MONO) - 96, pcm.data(), pcmBytes, decoded); + Require(decoded.result == 0 && decoded.totalDecodedSamples == 2 * 1152); + + const GaplessDecode skip{0, 100, 0}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &skip, sizeof(skip), result); + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + Require(result[0] == 0 && decoded.result == 0 && decoded.inputConsumed == 96 && decoded.outputWritten == (1152 - 100) * 2 && decoded.totalDecodedSamples == 1152 - 100); + + result[0] = -1; + RunControl(context, instance, CONTROL_RESET, nullptr, 0, result); + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + Require(result[0] == 0 && decoded.result == 0 && decoded.outputWritten == (1152 - 100) * 2 && decoded.totalDecodedSamples == 1152 - 100); + + const GaplessDecode none{0, 0, 0}; + Require(ControlThrows(instance, SIDEBAND_GAPLESS_DECODE, &none, sizeof(none), 8)); + + const GaplessDecode longer{5000, 200, 0}; + result[0] = -1; + RunControl(context, instance, CONTROL_RESET | SIDEBAND_GAPLESS_DECODE, &longer, sizeof(longer), result); + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + Require(result[0] == 0 && decoded.result == 0 && decoded.outputWritten == (1152 - 200) * 2 && decoded.totalDecodedSamples == 1152 - 200); + + const std::uint32_t parameters[2] = {1, 0}; + Require(ControlRefused(context, instance, CONTROL_INITIALIZE, parameters, sizeof(parameters))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestControlOpus(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const OpusControl start{{0, 0, 0}, 2, 48000, 0}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0); + std::vector pcm(960 * 2); + DecodeSideband decoded{}; + RunDecode(context, instance, OPUS_STEREO, sizeof(OPUS_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + const std::size_t bytes = OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == 2 + bytes && decoded.outputWritten == 960 * 2 * 2); + Require(ControlRefused(context, instance, CONTROL_START, &start, sizeof(start) - sizeof(start.third))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + } int main() { @@ -489,5 +641,8 @@ int main() { TestCodecInfo(context); TestBatchWaitRelease(context); TestBatchCancel(context); + TestControlAt9(context); + TestControlMp3(context); + TestControlOpus(context); Require(sceAjmFinalize(context) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..43ec7b4d0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -106,6 +106,10 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws - [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read From 99260427f402560389910a401a3a5c9910f86036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:45:44 +0200 Subject: [PATCH 077/203] feat(libc): support %n in wide formatting Co-Authored-By: Claude --- core/libs/prx/libc/src/FormattingWide.cpp | 14 ++++++++++--- core/libs/tests/GuestWideFormatting.cpp | 24 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index ad53b9862..35912693c 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -89,7 +89,7 @@ void AppendPadded(std::u16string& out, const std::u16string& text, bool left, in if (left) out.append(pad, u' '); } -std::u16string FormatWide(const char16_t* format, VaList* source, bool rejectNullStrings = false) { +std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = false) { if (format == nullptr || source == nullptr) throw std::invalid_argument("Null formatting argument"); LibcDetail::FormatArguments args(source); std::u16string out; @@ -172,15 +172,23 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool rejectNul const std::size_t limit = precision < 0 ? SIZE_MAX : static_cast(precision); if (conversion == u'S' || length == "l") { const char16_t* value = args.Next(); - if (value == nullptr && rejectNullStrings) throw std::invalid_argument("Null string argument"); + if (value == nullptr && secure) throw std::invalid_argument("Null string argument"); if (value == nullptr) value = u"(null)"; for (std::size_t index = 0; index < limit && value[index] != 0; ++index) text.push_back(value[index]); } else { const char* value = args.Next(); - if (value == nullptr && rejectNullStrings) throw std::invalid_argument("Null string argument"); + if (value == nullptr && secure) throw std::invalid_argument("Null string argument"); AppendUtf16(text, value != nullptr ? value : "(null)", limit); } AppendPadded(out, text, left, width); + } else if (conversion == u'n' && integerLength && spec == "%" && !secure) { + void* pointer = args.Next(); + if (!pointer) throw std::invalid_argument("Null format count pointer"); + const int count = static_cast(out.size()); + if (length == "hh") *static_cast(pointer) = static_cast(count); + else if (length == "h") *static_cast(pointer) = static_cast(count); + else if (length.empty()) *static_cast(pointer) = count; + else *static_cast(pointer) = count; } else { throw std::invalid_argument("Unsupported format conversion"); } diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 343ef0e51..c758ba10b 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -88,8 +88,32 @@ static void CheckBounded() { Require(Format(buffer, 16, u"%s", nullNarrow) == 6 && buffer == std::u16string(u"(null)"), "vswprintf null %s argument"); } +static void CheckCount() { + char16_t buffer[16]; + int count = -7; + Require(Format(buffer, 16, u"ab%ncd", &count) == 4 && buffer == std::u16string(u"abcd") && count == 2, "vswprintf %n"); + count = -7; + Require(Format(buffer, 16, u"%s%n!", "\xf0\x9f\x98\x80", &count) == 3 && count == 2, "vswprintf %n counts UTF-16 units"); + count = -7; + Require(Format(buffer, 16, u"%n", &count) == 0 && buffer[0] == 0 && count == 0, "vswprintf %n at start"); + unsigned char bytes[2] = {0xaa, 0xaa}; + Require(Format(buffer, 16, u"%3d%hhn", 5, bytes) == 3 && bytes[0] == 3 && bytes[1] == 0xaa, "vswprintf %hhn"); + unsigned short halves[2] = {0xaaaa, 0xaaaa}; + Require(Format(buffer, 16, u"%5d%hn", 5, halves) == 5 && halves[0] == 5 && halves[1] == 0xaaaa, "vswprintf %hn"); + long long wide = -1; + Require(Format(buffer, 16, u"%4d%lln", 5, &wide) == 4 && wide == 4, "vswprintf %lln"); + wide = -1; + Require(Format(buffer, 16, u"%2d%ln", 5, &wide) == 2 && wide == 2, "vswprintf %ln"); + count = -7; + Require(Format(buffer, 16, u"a%5n", &count) < 0 && count == -7, "vswprintf %n with a width"); + Require(Format(buffer, 16, u"a%Ln", &count) < 0 && count == -7, "vswprintf %Ln"); + const int* const nullCount = nullptr; + Require(Format(buffer, 16, u"a%n", nullCount) < 0, "vswprintf null %n argument"); +} + int main() { CheckBounded(); + CheckCount(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); Check(u"%.0s", "\xc3\xa9", u""); From a546bbef93d561fc3857009105ef3c312ee064b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:53:47 +0200 Subject: [PATCH 078/203] feat(libc): implement collate::_Getcat Co-Authored-By: Claude --- core/libs/prx/libc/CMakeLists.txt | 13 ++ core/libs/prx/libc/include/GuestLocale.hpp | 30 ++++ core/libs/prx/libc/src/LocaleSupport.cpp | 82 ++++++++++- core/libs/tests/GuestCollate.cpp | 156 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestCollate.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 575971f71..d019b3bf7 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -232,6 +232,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_collate_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestCollate.cpp) +target_include_directories(guest_collate_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_collate_tests PRIVATE libc) +configure_windows_unwind(guest_collate_tests) +set_target_properties(guest_collate_tests PROPERTIES CXX_EXTENSIONS OFF) +if(BUILD_TESTING) + set_target_properties(guest_collate_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_collate COMMAND guest_collate_tests) + if(WIN32) + set_tests_properties(guest_collate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(application_heap_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/ApplicationHeap.cpp) target_include_directories(application_heap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(application_heap_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 1e46c9210..45e29df3f 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -35,6 +35,29 @@ struct Implementation { const char* name; }; +struct String { + std::uint64_t reserved; + union { + char buffer[16]; + char* pointer; + }; + std::uint64_t size; + std::uint64_t capacity; +}; + +struct CollateFacet { + Facet base; + const void* collation; + const void* wideCollation; +}; + +struct CollateVtable { + FacetVtable facet; + int (APS5_VABI *compare)(const CollateFacet* self, const char* first1, const char* last1, const char* first2, const char* last2); + String* (APS5_VABI *transform)(String* result, const CollateFacet* self, const char* first, const char* last); + std::int64_t (APS5_VABI *hash)(const CollateFacet* self, const char* first, const char* last); +}; + struct IosBase { const void* vtable; std::int64_t standardStream; @@ -59,6 +82,13 @@ static_assert(offsetof(IosBase, storage) == 0x28); static_assert(offsetof(IosBase, callbacks) == 0x30); static_assert(offsetof(IosBase, locale) == 0x38); static_assert(sizeof(IosBase) == 0x40); +static_assert(offsetof(String, buffer) == 0x8); +static_assert(offsetof(String, size) == 0x18); +static_assert(sizeof(String) == 0x28); +static_assert(offsetof(CollateFacet, collation) == 0x10); +static_assert(sizeof(CollateFacet) == 0x20); +static_assert(offsetof(CollateVtable, compare) == 0x20); +static_assert(offsetof(CollateVtable, hash) == 0x30); static_assert(alignof(LocinfoStorage) == 8); } diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index abc4717b0..941df32b3 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -13,12 +13,19 @@ #include #include #include +#include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/GuestLocale.hpp" #include "SceTypes.hpp" +extern "C" { +void* APS5_VABI _Znwm_nid_postfix(std::size_t size); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +} + namespace { std::mutex g_localeInitMutex; @@ -71,6 +78,71 @@ const GuestLocale::FacetVtable g_copiedLocaleVtable{DestroyCopiedLocale, Destroy GuestLocale::Facet* g_classicFacets[1]{}; GuestLocale::Implementation g_classicLocale{{&g_localeVtable, 1, 0}, g_classicFacets, 1, 0, false, "C"}; +constexpr std::size_t GuestStringInlineCapacity = 15; +constexpr std::size_t GuestStringLargeAllocation = 0x1000; + +void ValidateCharacterRange(const char* first, const char* last, const char* function) { + if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); +} + +void APS5_VABI DestroyCollate(GuestLocale::Facet*) {} + +void APS5_VABI DeleteCollate(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("collate delete: null facet"); + _ZdlPv_nid_postfix(self); +} + +void APS5_VABI RetainFacet(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("facet retain: null facet"); + std::atomic_ref(self->references).fetch_add(1); +} + +GuestLocale::Facet* APS5_VABI ReleaseFacet(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("facet release: null facet"); + return std::atomic_ref(self->references).fetch_sub(1) == 1 ? self : nullptr; +} + +int APS5_VABI CollateCompare(const GuestLocale::CollateFacet*, const char* first1, const char* last1, const char* first2, const char* last2) { + ValidateCharacterRange(first1, last1, "collate::do_compare"); + ValidateCharacterRange(first2, last2, "collate::do_compare"); + const auto length1 = static_cast(last1 - first1); + const auto length2 = static_cast(last2 - first2); + const auto common = std::min(length1, length2); + const int order = common == 0 ? 0 : std::memcmp(first1, first2, common); + if (order != 0) return order < 0 ? -1 : 1; + if (length1 == length2) return 0; + return length1 < length2 ? -1 : 1; +} + +GuestLocale::String* APS5_VABI CollateTransform(GuestLocale::String* result, const GuestLocale::CollateFacet*, const char* first, const char* last) { + if (result == nullptr) throw std::invalid_argument("collate::do_transform: null result"); + ValidateCharacterRange(first, last, "collate::do_transform"); + const auto length = static_cast(last - first); + if (length != 0 && std::memchr(first, 0, length) != nullptr) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); + if (length + 1 >= GuestStringLargeAllocation) throw std::runtime_error("collate::do_transform: results of 4095 characters or more are not supported"); + char* data = result->buffer; + std::size_t capacity = GuestStringInlineCapacity; + if (length > GuestStringInlineCapacity) { + capacity = length; + data = static_cast(_Znwm_nid_postfix(capacity + 1)); + } + if (length != 0) std::memcpy(data, first, length); + data[length] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* first, const char* last) { + ValidateCharacterRange(first, last, "collate::do_hash"); + std::uint64_t hash = 0xcbf29ce484222325; + for (; first != last; ++first) hash = (hash ^ static_cast(*first)) * 0x100000001b3; + return static_cast(hash); +} + +const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -189,8 +261,14 @@ void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(Gu NotImplemented_nid_no_patch(__func__); } -void APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { + if (facet != nullptr && *facet == nullptr) { + if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); + auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); + *collate = {{&g_collateVtable.facet, 0, 0}, nullptr, nullptr}; + *facet = &collate->base; + } + return 1; } void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName) { diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp new file mode 100644 index 000000000..ed4b1976d --- /dev/null +++ b/core/libs/tests/GuestCollate.cpp @@ -0,0 +1,156 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; +std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +} + +namespace { + +std::size_t allocations = 0; +std::size_t frees = 0; +std::size_t lastSize = 0; +void* lastAllocation = nullptr; +void* lastFree = nullptr; + +void require(bool condition) { + if (!condition) std::abort(); +} + +template +void reject(TAction action) { + bool rejected = false; + try { action(); } catch (const std::exception&) { rejected = true; } + require(rejected); +} + +void* APS5_VABI allocate(std::size_t bytes) { + ++allocations; + lastSize = bytes; + lastAllocation = std::malloc(bytes); + return lastAllocation; +} + +void APS5_VABI release(void* pointer) { + ++frees; + lastFree = pointer; + std::free(pointer); +} + +void* APS5_VABI allocateZeroed(std::size_t, std::size_t) { std::abort(); } +void* APS5_VABI reallocate(void*, std::size_t) { std::abort(); } +void* APS5_VABI align(std::size_t, std::size_t) { std::abort(); } +void* APS5_VABI realign(void*, std::size_t, std::size_t) { std::abort(); } +int APS5_VABI posixAlign(void**, std::size_t, std::size_t) { std::abort(); } + +const GuestLocale::CollateVtable& Vtable(const GuestLocale::Facet* facet) { + return *reinterpret_cast(facet->vtable); +} + +int Compare(const GuestLocale::CollateFacet* facet, const std::string& left, const std::string& right) { + return Vtable(&facet->base).compare(facet, left.data(), left.data() + left.size(), right.data(), right.data() + right.size()); +} + +std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& text) { + return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); +} + +} + +int main() { + const std::array api{reinterpret_cast(&allocate), reinterpret_cast(&release), reinterpret_cast(&allocateZeroed), reinterpret_cast(&reallocate), reinterpret_cast(&align), reinterpret_cast(&realign), reinterpret_cast(&posixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + + const GuestLocale::Implementation* const* classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); + GuestLocale::Facet existing{}; + GuestLocale::Facet* facet = &existing; + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet == &existing && allocations == 0); + + GuestLocale::Implementation french = *_ZSt21_sceLibcClassicLocale_nid_postfix; + french.name = "fr_FR"; + const GuestLocale::Implementation* frenchPointer = &french; + facet = nullptr; + reject([&] { _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, &frenchPointer); }); + require(facet == nullptr && allocations == 0); + + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet != nullptr && allocations == 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); + const auto* collate = reinterpret_cast(facet); + require(facet->references == 0 && collate->collation == nullptr && collate->wideCollation == nullptr); + + require(Compare(collate, "abc", "abd") == -1); + require(Compare(collate, "abd", "abc") == 1); + require(Compare(collate, "a", "c") == -1); + require(Compare(collate, "abc", "abc") == 0); + require(Compare(collate, "ab", "abc") == -1); + require(Compare(collate, "abc", "ab") == 1); + require(Compare(collate, "", "") == 0); + require(Compare(collate, "", "a") == -1); + require(Compare(collate, "\x80", "a") == 1); + require(Compare(collate, std::string("a\0", 2), "a") == 1); + require(Compare(collate, std::string("a\0b", 3), std::string("a\0c", 3)) == -1); + require(Vtable(facet).compare(collate, nullptr, nullptr, nullptr, nullptr) == 0); + const char text[] = "abc"; + reject([&] { Vtable(facet).compare(collate, text + 2, text, text, text + 1); }); + reject([&] { Vtable(facet).compare(collate, nullptr, text, text, text + 1); }); + + require(Hash(collate, "") == 0xcbf29ce484222325ull); + require(Hash(collate, "a") == 0xaf63dc4c8601ec8cull); + require(Hash(collate, "foobar") == 0x85944171f73967e8ull); + require(Hash(collate, "\xff") == ((0xcbf29ce484222325ull ^ 0xffull) * 0x100000001b3ull)); + reject([&] { Vtable(facet).hash(collate, text + 1, text); }); + + GuestLocale::String result; + std::memset(&result, 0xcd, sizeof(result)); + const std::string shortText = "hello"; + require(Vtable(facet).transform(&result, collate, shortText.data(), shortText.data() + shortText.size()) == &result); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 5 && result.capacity == 15 && std::strcmp(result.buffer, "hello") == 0); + require(allocations == 1); + + std::memset(&result, 0xcd, sizeof(result)); + require(Vtable(facet).transform(&result, collate, nullptr, nullptr) == &result); + require(result.size == 0 && result.capacity == 15 && result.buffer[0] == 0); + + const std::string fifteen(15, 'q'); + require(Vtable(facet).transform(&result, collate, fifteen.data(), fifteen.data() + fifteen.size()) == &result); + require(result.size == 15 && result.capacity == 15 && std::string(result.buffer) == fifteen && allocations == 1); + + const std::string longText = "collation of a longer text"; + require(Vtable(facet).transform(&result, collate, longText.data(), longText.data() + longText.size()) == &result); + require(allocations == 2 && lastSize == longText.size() + 1 && result.pointer == lastAllocation); + require(result.size == longText.size() && result.capacity == longText.size() && std::string(result.pointer) == longText); + std::free(result.pointer); + + const std::string largest(4094, 'z'); + require(Vtable(facet).transform(&result, collate, largest.data(), largest.data() + largest.size()) == &result); + require(allocations == 3 && lastSize == 4095 && result.capacity == 4094 && std::string(result.pointer) == largest); + std::free(result.pointer); + + const std::string tooLong(4095, 'z'); + reject([&] { Vtable(facet).transform(&result, collate, tooLong.data(), tooLong.data() + tooLong.size()); }); + const std::string embedded("a\0b", 3); + reject([&] { Vtable(facet).transform(&result, collate, embedded.data(), embedded.data() + embedded.size()); }); + reject([&] { Vtable(facet).transform(nullptr, collate, text, text + 1); }); + require(allocations == 3); + + Vtable(facet).facet.retain(facet); + Vtable(facet).facet.retain(facet); + require(facet->references == 2); + require(Vtable(facet).facet.release(facet) == nullptr && facet->references == 1); + require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + Vtable(facet).facet.destroy(facet); + require(frees == 0); + Vtable(facet).facet.deleteObject(facet); + require(frees == 1 && lastFree == collate); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..8a521f867 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -169,6 +169,7 @@ Throughout the project, every function at every stage either **does exactly what - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached +- [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). Lines are allocated on the host, not in the font memory From 5a04d3cc9eaff1d81dd79e7ff4f0ddcfd84e211b Mon Sep 17 00:00:00 2001 From: Fishybeing <328519762+Fishybeing@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:45:13 +0200 Subject: [PATCH 079/203] build(windows): guard NOMINMAX before including windows.h Four sources defined NOMINMAX unconditionally, which redefines the macro the build already passes and warns on every MinGW build. Use the #ifndef guard the rest of the tree uses. --- core/libs/prx/libc/src/Filesystem.cpp | 2 ++ core/libs/prx/libkernel/DirectMemory/Export.cpp | 2 ++ core/libs/prx/libkernel/Module/src/DynamicLoader.cpp | 2 ++ core/libs/tests/GuestAccess.cpp | 2 ++ 4 files changed, 8 insertions(+) diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index 258f10c3f..d7f191170 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -2,7 +2,9 @@ #include #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #else #include diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 096519100..1cf4a14f0 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -7,7 +7,9 @@ #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX #define NOMINMAX +#endif #include #else #include diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 24a842615..d2604c94d 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -9,7 +9,9 @@ #include #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #else #include diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index 723b9c090..0cce786ad 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -3,7 +3,9 @@ #include #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #include #endif From 5f1c45c22355b27fd5b15f348881a3c48168eac6 Mon Sep 17 00:00:00 2001 From: Fishybeing <328519762+Fishybeing@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:46:13 +0200 Subject: [PATCH 080/203] fix(libkernel): correct the MemoryPool.hpp include guard The guard tested CORE_LIBS_PRX_LIBKERNEL_... but defined CORE_LIBS_PRX_LINKERNEL_..., so it never took effect and GCC reports -Wheader-guard. --- core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp index b6e799e68..5882f0c2e 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp @@ -1,5 +1,5 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP -#define CORE_LIBS_PRX_LINKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP #include #include From 38824ec52450f38d93308c8c4c7f351d3c7e5718 Mon Sep 17 00:00:00 2001 From: Fishybeing <328519762+Fishybeing@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:54:55 +0200 Subject: [PATCH 081/203] fix(http): count sceHttpParseStatusLine as implemented Its only NotImplemented_nid_no_patch call guarded the int range limit on the consumed byte count, so tools/progress.py listed the implemented function as missing. Throw std::out_of_range like sceHttpParseResponseHeader does for the same limit. --- core/libs/prx/libSceHttp/src/Parse.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceHttp/src/Parse.cpp b/core/libs/prx/libSceHttp/src/Parse.cpp index 93b9d109b..0732f6240 100644 --- a/core/libs/prx/libSceHttp/src/Parse.cpp +++ b/core/libs/prx/libSceHttp/src/Parse.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" @@ -56,7 +57,7 @@ int APS5_VABI sceHttpParseStatusLine(const char* statusLine, size_t lineLen, int size_t length = static_cast(static_cast(lineFeed) - phrase); const size_t consumed = index + length + 1; if (consumed > static_cast(INT32_MAX)) { - NotImplemented_nid_no_patch("sceHttpParseStatusLine: status line longer than INT32_MAX"); + throw std::out_of_range("sceHttpParseStatusLine: consumed byte count exceeds int"); } if (length > 0 && phrase[length - 1] == '\r') --length; From 7389b2581f48bb29a9bdf23768a89f1420920712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 14:59:37 +0200 Subject: [PATCH 082/203] feat(libc): implement std::_Fiopen Co-Authored-By: Claude --- core/libs/prx/libc/src/CxxAbiSupport.cpp | 8 -- core/libs/prx/libc/src/FileAndHeap.cpp | 27 +++++ core/libs/tests/GuestFiopen.cpp | 137 +++++++++++++++++++++-- docs/dev/TechnicalDebt.md | 1 - 4 files changed, 156 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index fd29fb581..008cc422e 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -42,14 +42,6 @@ int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (*destructor)(void const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std::iostream_category(); } -std::FILE* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char* filename, int mode, int protection) { - static_cast(filename); - static_cast(mode); - static_cast(protection); - NotImplemented_nid_no_patch("std::_Fiopen"); - return nullptr; -} - int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix( int* flag, int (APS5_VABI *callback)(void*, void*, void**), void* arg ) { diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 80f1716a1..42b407643 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -106,6 +106,33 @@ int APS5_VABI fclose_nid_postfix(FileStream* stream) { return 0; } +int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); + +FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char* filename, int mode, int protection) { + static_cast(protection); + constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, + Binary = 0x80; + constexpr std::pair modes[] = { + {In, "r"}, {Out, "w"}, {Out | Trunc, "w"}, {Out | App, "a"}, + {In | Binary, "rb"}, {Out | Binary, "wb"}, {Out | Trunc | Binary, "wb"}, {Out | App | Binary, "ab"}, + {In | Out, "r+"}, {In | Out | Trunc, "w+"}, {In | Out | App, "a+"}, + {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}}; + const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); + const char* openMode = nullptr; + for (const auto& [flags, text] : modes) if (flags == open) openMode = text; + if (!openMode) return nullptr; + if ((mode & Noreplace) && (open & Out)) { + if (auto* existing = fopen_nid_postfix(filename, "r")) { + fclose_nid_postfix(existing); + return nullptr; + } + } + auto* stream = fopen_nid_postfix(filename, openMode); + if (!stream || !(mode & Ate) || fseek_nid_postfix(stream, 0, SEEK_END) == 0) return stream; + fclose_nid_postfix(stream); + return nullptr; +} + size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 379cda811..6c2636ae3 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -1,15 +1,136 @@ +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include -#include -#include +#include +#include +#include +#include -extern "C" std::FILE* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); +extern "C" { +FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(FileStream*); +std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Fiopen check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, + Binary = 0x80; + +static FileStream* Open(const char* name, int mode) { + return _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(name, mode, 0x1b6); +} + +static std::string Contents(const char* name) { + auto* stream = fopen_nid_postfix(name, "rb"); + Require(stream != nullptr); + std::string text; + for (int c; (c = std::fgetc(stream->GetHandle())) != EOF;) text += static_cast(c); + fclose_nid_postfix(stream); + return text; +} + +static void Write(FileStream* stream, const char* text) { + Require(stream != nullptr); + Require(std::fputs(text, stream->GetHandle()) >= 0); + fclose_nid_postfix(stream); +} + +static bool Exists(const char* name) { + auto* stream = fopen_nid_postfix(name, "r"); + if (stream) fclose_nid_postfix(stream); + return stream != nullptr; +} int main() { - try { - _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix("unused", 1, 0); - } catch (const std::runtime_error& error) { - return std::string_view(error.what()) == "std::_Fiopen not implemented" ? 0 : 1; + const std::filesystem::path dir = "guest_fiopen_dir"; + std::filesystem::remove_all(dir); + std::filesystem::create_directory(dir); + const char* file = "guest_fiopen_dir/a"; + const char* missing = "guest_fiopen_dir/missing"; + + for (int mode : {0, Ate, Trunc, Binary, Nocreate, Noreplace, In | Trunc, In | Trunc | Binary, Out | App | Trunc, + In | Out | App | Trunc, Trunc | Binary, App | Trunc}) { + Require(Open(missing, mode) == nullptr); + Require(!Exists(missing)); } - return 1; + + Write(Open(file, Out), "abc"); + Require(Contents(file) == "abc"); + + auto* stream = Open(file, In); + Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'a'); + fclose_nid_postfix(stream); + stream = Open(file, In); + Require(stream != nullptr); + std::fputc('x', stream->GetHandle()); + fclose_nid_postfix(stream); + Require(Contents(file) == "abc"); + + Write(Open(file, App), "d"); + Require(Contents(file) == "abcd"); + Write(Open(file, Out | App | Binary), "e"); + Require(Contents(file) == "abcde"); + + stream = Open(file, In | Ate); + Require(stream != nullptr && ftello_nid_postfix(stream) == 5); + fclose_nid_postfix(stream); + stream = Open(file, In | Binary); + Require(stream != nullptr && ftello_nid_postfix(stream) == 0); + fclose_nid_postfix(stream); + + Require(Open(file, Out | Noreplace) == nullptr); + Require(Open(file, In | Out | Binary | Noreplace) == nullptr); + Require(Open(file, App | Noreplace) == nullptr); + Require(Contents(file) == "abcde"); + stream = Open(file, In | Noreplace); + Require(stream != nullptr); + fclose_nid_postfix(stream); + + Require(Open(missing, Out | Nocreate) == nullptr); + Require(!Exists(missing)); + Write(Open(file, Out | Nocreate), "X"); + Require(Contents(file) == "Xbcde"); + stream = Open(file, Out | Nocreate | Binary | Ate); + Require(stream != nullptr && ftello_nid_postfix(stream) == 5); + Write(stream, "f"); + Require(Contents(file) == "Xbcdef"); + + stream = Open(file, In | Out); + Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'X'); + fclose_nid_postfix(stream); + Require(Contents(file) == "Xbcdef"); + + Write(Open(file, In | Out | App), "g"); + Require(Contents(file) == "Xbcdefg"); + + Write(Open(file, In | Out | Trunc | Binary), "h"); + Require(Contents(file) == "h"); + Write(Open(file, Out | Trunc), "ij"); + Require(Contents(file) == "ij"); + Write(Open(file, Out | Binary), "k"); + Require(Contents(file) == "k"); + + Write(Open(missing, Out | Noreplace), "new"); + Require(Contents(missing) == "new"); + Write(Open(missing, Out | Trunc | Nocreate), "w+"); + Require(Contents(missing) == "w+"); + std::filesystem::remove(dir / "missing"); + Write(Open(missing, Out | Trunc | Nocreate), "created"); + Require(Contents(missing) == "created"); + + stream = Open(file, In | 0x100 | 0x8000); + Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); + fclose_nid_postfix(stream); + + std::filesystem::remove_all(dir); + return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..be736a8ad 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -73,7 +73,6 @@ Throughout the project, every function at every stage either **does exactly what - [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [std::_Fiopen](../../core/libs/prx/libc/src/CxxAbiSupport.cpp) (libc) - `_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi` (NID `vYWK2Pz8vGE`; the name comes from the public NID list): the values of the `_Openmode` argument and the meaning of the protection argument are unknown, so it throws when called. It is exported so that a title that imports it (PPSA21564 and PPSA21567 do) loads against the host libc when its own `libc.prx` is left out of the guest conversion. - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export From c7abfa89b23fa410cc4a6b9c3224d9c12cc63e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 15:12:53 +0200 Subject: [PATCH 083/203] feat(libSceRazorCpu): implement sceRazorCpuFlushOccurred Co-Authored-By: Claude --- core/libs/prx/libSceRazorCpu/Export.cpp | 5 +++++ core/libs/tests/GuestRazorCpu.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 0a5ec3953..90663520e 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -35,4 +35,9 @@ int APS5_VABI sceRazorCpuPopMarker(void) { return 0; } +int APS5_VABI sceRazorCpuFlushOccurred(uint64_t* timeSpentInFlush) { + if (timeSpentInFlush != nullptr) *timeSpentInFlush = 0; + return 0; +} + } diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 0f1469a82..81dcc1780 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -9,6 +9,7 @@ int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); +int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); } namespace { @@ -28,4 +29,8 @@ int main() { Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarkerStatic("unbalanced", 0x80ffffffu, 2) == 0); Require(sceRazorCpuIsCapturing() == 0); + std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; + Require(sceRazorCpuFlushOccurred(&timeSpentInFlush) == 0); + Require(timeSpentInFlush == 0); + Require(sceRazorCpuFlushOccurred(nullptr) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..f5697d02f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -102,6 +102,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info +- [sceRazorCpuFlushOccurred](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts From 04ed454f75e2d24800c6887f9eece427c87cc1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 15:16:45 +0200 Subject: [PATCH 084/203] feat(libSceSystemService): implement Disable/ReenableMusicPlayer Co-Authored-By: Claude --- core/libs/prx/libSceSystemService/Export.cpp | 6 ++---- core/libs/tests/GuestSystemService.cpp | 6 ++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 19358d5c1..0de0c37f4 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -136,8 +136,7 @@ int APS5_VABI sceSystemServiceLaunchPlayerDialog(const void* param) { } int APS5_VABI sceSystemServiceDisableMusicPlayer(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { @@ -151,8 +150,7 @@ int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { } int APS5_VABI sceSystemServiceReenableMusicPlayer(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceShowControllerSettings(void) { diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index cd875318c..6341c8d40 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -25,6 +25,8 @@ bool ParamGetStringThrows(int paramId, char* buf, std::size_t bufSize) { extern "C" int APS5_VABI sceSystemServicePowerTick(void); extern "C" int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info); +extern "C" int APS5_VABI sceSystemServiceDisableMusicPlayer(void); +extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); int main() { Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); @@ -32,6 +34,10 @@ int main() { Require(sceSystemServiceReportAbnormalTermination(nullptr) == SYSTEM_SERVICE_OK); int info = 0; Require(sceSystemServiceReportAbnormalTermination(&info) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceGetHdrToneMapLuminance(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); SystemServiceHdrToneMapLuminance luminance{-1.0f, -1.0f, -1.0f}; Require(sceSystemServiceGetHdrToneMapLuminance(&luminance) == SYSTEM_SERVICE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..8e908c40a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -18,7 +18,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing -- [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick` and `sceSystemServiceReportAbnormalTermination` return success and do nothing: the host has no auto power-off timer to reset and no system crash reporter to notify +- [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer` and `sceSystemServiceReenableMusicPlayer` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify and no system music player to pause or resume - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them From 736ba56d850008256fc66af93b7ca2f3c51f6464 Mon Sep 17 00:00:00 2001 From: Merijn Date: Tue, 6 Oct 2026 15:20:51 +0200 Subject: [PATCH 085/203] fix(avplayer): deliver the frame at a jump while paused A paused player returned no video at all. Unity's video player prepares a clip by starting it, pausing, jumping to the start and polling sceAvPlayerGetVideoDataEx until the frame at the jump arrives, and only then resumes; PPSA12544 waited there forever on a black screen in its intro cinematic. While paused, the first frame after a jump is now delivered (the paused clock rebased to it); other frames are still held until the player resumes. The guest test pauses, jumps and expects that one frame and nothing after it. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAvPlayer/src/Player.cpp | 2 +- core/libs/prx/libSceAvPlayer/src/Source.cpp | 3 ++- core/libs/tests/GuestAvPlayer.cpp | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index cc55f6eff..1e265d395 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -327,7 +327,7 @@ int Player::SetAvSyncMode(std::uint32_t mode) { bool Player::GetVideoData(AvPlayerFrameInfoEx& info) { std::lock_guard lock(mutex); - if (!source || state != State::Play) return false; + if (!source || (state != State::Play && state != State::Pause)) return false; return source->GetVideoData(info); } diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index a59c07a47..8990d5d57 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -494,7 +494,7 @@ private: } bool takeVideo(AvPlayerFrameInfoEx& info) { - if (!started || paused || video.stream < 0 || speed < 0) return false; + if (!started || video.stream < 0 || speed < 0) return false; if (speed != NormalSpeed) dropLateAudio(); auto& frames = video.frames; const bool synced = syncMode == SyncModeDefault; @@ -502,6 +502,7 @@ private: while (!frames.empty() && frames.front().epoch < stale) recycleFront(video); if (frames.empty()) return false; const auto& front = frames.front(); + if (paused && front.epoch <= clockEpoch) return false; if (front.epoch > clockEpoch) { if (synced && audioDriving && speed == NormalSpeed) return false; if (synced && front.seamless && presented && presentationClock.Now() < static_cast(lastPresented + frameDuration())) return false; diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index e9981ef53..b38ee2895 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -756,6 +756,13 @@ void TestPlayback() { Check(!sceAvPlayerGetVideoDataEx(player, &frame) && !sceAvPlayerGetAudioData(player, &sound), "data delivered while paused"); Check(sceAvPlayerCurrentTime(player) == paused, "clock moved while paused"); Check(sceAvPlayerIsActive(player), "inactive while paused"); + Check(sceAvPlayerJumpToTime(player, 300) == 0, "jump while paused failed"); + Check(WaitFor([&] { return events.Seen(EventWarning, JumpComplete); }), "jump completion missing while paused"); + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame at a jump while paused"); + CheckVideoFrame(frame); + Check(frame.timestamp >= 300 && frame.timestamp <= 367, "paused jump landed at " + std::to_string(frame.timestamp)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Check(!sceAvPlayerGetVideoDataEx(player, &frame) && !sceAvPlayerGetAudioData(player, &sound), "data delivered after the frame at a paused jump"); events.Clear(); Check(sceAvPlayerResume(player) == 0, "resume failed"); Check(sceAvPlayerResume(player) == OperationFailed, "resume accepted while playing"); From 0ec3061a468b4ee44bb0d09674f0dfe51a5cfc8f Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 6 Oct 2026 15:31:36 +0200 Subject: [PATCH 086/203] fix(recompiler): merge a loop selection where its other arm joins or leaves A selection inside a loop whose arm either joins the other target or continues/breaks (`if (a) { if (b) continue; x; }`) got the nearest common post-dominator as its merge. When the loop exits after the outer join, that is the outer selection's merge, so both selections shared it. splitSharedMergeBlocks then inserted a forwarding block in front of it, recomputed the same merge, and repeated until the budget ran out: PPSA14632's compute shader 0x1a80ee0000 stopped with "CFG shared merge splitting exceeded budget: originalBlocks=92 currentBlocks=460". findSelectionMerge now picks the joined target when the other arm is a conditional block with the header as its only predecessor, and every successor of that arm is the joined target or the loop's continue or merge block. The shader structures with 13 added blocks and no clones or route variables. In the result the outer selection header dominates the loop's continue target, so the test's verifyStructured counted the continue target as a selection member that branches outside (back to the loop header). SPIR-V uses structural dominance, where the loop header's edge to its continue target keeps it out of the selection, so verifyStructured now skips the continue construct of an enclosing loop. spirv-val accepts the new test's SPIR-V for both subgroup sizes. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 31 +++++++++++++++++++ .../ControlFlow/src/Structurizer.cpp | 13 ++++++++ 2 files changed, 44 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index ee6cd1e82..df874dd91 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -53,6 +53,13 @@ std::vector constructBlocks(const ControlFlowGraph& graph, std::u return blocks; } +bool inEnclosingContinueConstruct(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t member) { + return std::any_of(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& loop) { + const auto continueBlock = loop.terminator.continueBlock; + return loop.terminator.loopHeader && graph.Dominates(loop.id, header) && !graph.Dominates(continueBlock, header) && graph.Dominates(continueBlock, member); + }); +} + void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph) { std::map mergeOwners; std::vector> loopExits; @@ -66,6 +73,7 @@ void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph) const auto& terminator = block.terminator; if (terminator.loopHeader || terminator.mergeBlock == InvalidControlFlowId) continue; for (const auto member : constructBlocks(graph, block.id, terminator.mergeBlock)) { + if (inEnclosingContinueConstruct(graph, block.id, member)) continue; for (const auto successor : graph.FindBlock(member).successors) { if (successor == terminator.mergeBlock || (graph.Dominates(block.id, successor) && !graph.Dominates(terminator.mergeBlock, successor))) continue; const bool loopExit = std::any_of(loopExits.begin(), loopExits.end(), [&](const auto& exits) { return successor == exits.first || successor == exits.second; }); @@ -363,6 +371,29 @@ latch: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860004u, 0x7d880088u, 0xbf870003u, 0x4a020281u, 0xbf820002u, 0x4a020282u, 0x4a020283u, 0x80089008u, 0xbf0ac008u, 0xbf85fff5u}), Split::Clone}, + {"continue beside the inner merge of a nested selection in a loop", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 +loop: + v_cmp_gt_u32 vcc, s8, v0 + s_cbranch_vccz join + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccnz inner + v_cmp_gt_u32 vcc, 4, v0 + s_cbranch_vccnz latch +inner: + v_add_nc_u32 v1, 1, v1 +join: + v_add_nc_u32 v1, 2, v1 + s_cmp_ge_u32 s8, 64 + s_cbranch_scc1 done +latch: + s_add_u32 s8, s8, 16 + s_branch loop +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860005u, 0x7d880088u, 0xbf870002u, 0x7d880084u, 0xbf870004u, 0x4a020281u, 0x4a020282u, 0xbf09c008u, 0xbf850002u, 0x80089008u, 0xbf82fff4u}), Split::None}, {"loop exit to the end of the program beside a kill exit", R"( v_mov_b32 v1, 0 s_mov_b64 s[20:21], exec diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 6f6eb09be..f23bd9258 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -464,6 +464,19 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock if (isLoopControlGateway(graph, *loop, falseTarget) && graph.Dominates(block.id, falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)) { return falseTarget; } + const auto joinsAt = [&](std::uint32_t join, std::uint32_t other) { + const auto& arm = graph.FindBlock(other); + const bool armLeavesOrJoins = arm.terminator.kind == TerminatorKind::ConditionalBranch && arm.predecessors.size() == 1u && std::all_of(arm.successors.begin(), arm.successors.end(), [&](std::uint32_t successor) { + return successor == join || successor == loop->continueBlock || successor == loop->mergeBlock; + }); + return armLeavesOrJoins && isInsideLoopConstruct(graph, *loop, other) && isInsideLoopConstruct(graph, *loop, join) && graph.Dominates(block.id, join); + }; + if (joinsAt(trueTarget, falseTarget)) { + return trueTarget; + } + if (joinsAt(falseTarget, trueTarget)) { + return falseTarget; + } if (globalMerge != InvalidControlFlowId) { const bool trueJoins = reachesWithinIteration(graph, *loop, trueTarget, globalMerge); const bool falseJoins = reachesWithinIteration(graph, *loop, falseTarget, globalMerge); From 8d7ff94689a870a9f925b1b5b644a4468369e64d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 14:02:11 +0000 Subject: [PATCH 087/203] feat(libSceKeyboard): translate the 106-key arrangement in sceKeyboardGetKey2Char The 106-key (Japanese) arrangement threw. Its alphanumeric layer now follows the JIS layout of the HID usages: the symbol keys right of the digits and letters, the Ro key (0x87) and the Yen key (0x89). With the Kana LED set it still throws, as the kana the console returns are unknown. Co-Authored-By: Claude Opus 5.5 --- .../libSceKeyboard/include/keyboard_structs.h | 1 + .../prx/libSceKeyboard/src/keyboard_impl.cpp | 15 ++++++++---- .../prx/libSceKeyboard/tests/Keyboard.cpp | 23 +++++++++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceKeyboard/include/keyboard_structs.h b/core/libs/prx/libSceKeyboard/include/keyboard_structs.h index 326fa4b84..c219f233d 100644 --- a/core/libs/prx/libSceKeyboard/include/keyboard_structs.h +++ b/core/libs/prx/libSceKeyboard/include/keyboard_structs.h @@ -18,6 +18,7 @@ constexpr std::int32_t KEYBOARD_ARRANGEMENT_106 = 1; constexpr std::uint32_t KEYBOARD_LED_NUM_LOCK = 1; constexpr std::uint32_t KEYBOARD_LED_CAPS_LOCK = 2; constexpr std::uint32_t KEYBOARD_LED_SCROLL_LOCK = 4; +constexpr std::uint32_t KEYBOARD_LED_KANA = 16; constexpr std::uint32_t KEYBOARD_MOD_LEFT_SHIFT = 2; constexpr std::uint32_t KEYBOARD_MOD_RIGHT_SHIFT = 32; diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 7bc369767..2bb6273e7 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -21,6 +21,8 @@ int keyCount = 0; constexpr char US_NORMAL[] = "1234567890\n\0\b\t -=[]\\\0;'`,./"; constexpr char US_SHIFTED[] = "!@#$%^&*()\n\0\b\t _+{}|\0:\"~<>?"; +constexpr char JIS_NORMAL[] = "1234567890\n\0\b\t -^@[]];:\0,./"; +constexpr char JIS_SHIFTED[] = "!\"#$%&'()\0\n\0\b\t =~`{}}+*\0<>?"; constexpr char KEYPAD_DIGITS[] = "1234567890."; KeyboardData snapshot() { @@ -69,15 +71,18 @@ bool updateKey(std::uint16_t keyCode, bool pressed) { return true; } -std::uint16_t usCharacter(std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode) { +std::uint16_t character(bool jis, std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode) { const bool shift = (modifierKey & (KEYBOARD_MOD_LEFT_SHIFT | KEYBOARD_MOD_RIGHT_SHIFT)) != 0; if (keyCode >= 0x04 && keyCode <= 0x1d) { const bool upper = shift != ((ledState & KEYBOARD_LED_CAPS_LOCK) != 0); return static_cast((upper ? 'A' : 'a') + keyCode - 0x04); } if (keyCode >= 0x1e && keyCode <= 0x38) { - return static_cast((shift ? US_SHIFTED : US_NORMAL)[keyCode - 0x1e]); + const char* table = jis ? (shift ? JIS_SHIFTED : JIS_NORMAL) : (shift ? US_SHIFTED : US_NORMAL); + return static_cast(table[keyCode - 0x1e]); } + if (jis && keyCode == 0x87) return shift ? '_' : '\\'; + if (jis && keyCode == 0x89) return shift ? '|' : '\\'; switch (keyCode) { case 0x54: return '/'; case 0x55: return '*'; @@ -155,13 +160,13 @@ int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledStat if (handle != KEYBOARD_HANDLE || !opened) return KEYBOARD_ERROR_INVALID_HANDLE; } if (charData == nullptr) return KEYBOARD_ERROR_INVALID_ARG; - if (arrange == KEYBOARD_ARRANGEMENT_106) { + if (arrange != KEYBOARD_ARRANGEMENT_101 && arrange != KEYBOARD_ARRANGEMENT_106) return KEYBOARD_ERROR_INVALID_ARG; + if (arrange == KEYBOARD_ARRANGEMENT_106 && (ledState & KEYBOARD_LED_KANA) != 0) { NotImplemented_nid_no_patch(__func__); return KEYBOARD_ERROR_INVALID_ARG; } - if (arrange != KEYBOARD_ARRANGEMENT_101) return KEYBOARD_ERROR_INVALID_ARG; *charData = {}; - charData->char_code = usCharacter(ledState, modifierKey, keyCode); + charData->char_code = character(arrange == KEYBOARD_ARRANGEMENT_106, ledState, modifierKey, keyCode); charData->processed = charData->char_code != 0; charData->length = charData->processed ? 1 : 0; return KEYBOARD_OK; diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 3b633176d..802e1da75 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -27,13 +27,17 @@ static SDL_Event KeyEvent(Uint32 type, SDL_Scancode scancode, Uint16 mod = 0, Ui return event; } -static std::uint16_t Char(std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode) { +static std::uint16_t Char(std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, std::int32_t arrange = KEYBOARD_ARRANGEMENT_101) { KeyboardCharData data{}; - Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, led, modifierKey, keyCode, &data) == KEYBOARD_OK); + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, arrange, led, modifierKey, keyCode, &data) == KEYBOARD_OK); Require(data.processed == (data.char_code != 0) && data.length == (data.processed ? 1 : 0)); return data.char_code; } +static std::uint16_t Jis(std::uint32_t modifierKey, std::uint16_t keyCode, std::uint32_t led = 0) { + return Char(led, modifierKey, keyCode, KEYBOARD_ARRANGEMENT_106); +} + int main() { KeyboardData data[16]{}; KeyboardCharData charData{}; @@ -115,7 +119,22 @@ int main() { Require(Char(0, 0, 0x59) == 0 && Char(KEYBOARD_LED_NUM_LOCK, 0, 0x59) == '1'); Require(Char(KEYBOARD_LED_NUM_LOCK, 0, 0x62) == '0' && Char(KEYBOARD_LED_NUM_LOCK, 0, 0x63) == '.'); Require(Char(0, 0, 0x54) == '/' && Char(0, 0, 0x57) == '+' && Char(0, 0, 0x58) == '\n'); + Require(Char(0, 0, 0x87) == 0 && Char(0, 0, 0x89) == 0); + + constexpr std::uint32_t shift = KEYBOARD_MOD_LEFT_SHIFT; + Require(Jis(0, 0x04) == 'a' && Jis(shift, 0x04) == 'A' && Jis(KEYBOARD_MOD_RIGHT_SHIFT, 0x04, KEYBOARD_LED_CAPS_LOCK) == 'a'); + Require(Jis(0, 0x1e) == '1' && Jis(shift, 0x1e) == '!' && Jis(shift, 0x1f) == '"' && Jis(shift, 0x23) == '&'); + Require(Jis(shift, 0x24) == '\'' && Jis(shift, 0x25) == '(' && Jis(shift, 0x26) == ')' && Jis(0, 0x27) == '0' && Jis(shift, 0x27) == 0); + Require(Jis(0, 0x28) == '\n' && Jis(0, 0x29) == 0 && Jis(0, 0x2a) == '\b' && Jis(0, 0x2b) == '\t' && Jis(shift, 0x2c) == ' '); + Require(Jis(0, 0x2d) == '-' && Jis(shift, 0x2d) == '=' && Jis(0, 0x2e) == '^' && Jis(shift, 0x2e) == '~'); + Require(Jis(0, 0x2f) == '@' && Jis(shift, 0x2f) == '`' && Jis(0, 0x30) == '[' && Jis(shift, 0x30) == '{'); + Require(Jis(0, 0x31) == ']' && Jis(shift, 0x31) == '}' && Jis(0, 0x32) == ']' && Jis(shift, 0x32) == '}'); + Require(Jis(0, 0x33) == ';' && Jis(shift, 0x33) == '+' && Jis(0, 0x34) == ':' && Jis(shift, 0x34) == '*'); + Require(Jis(0, 0x35) == 0 && Jis(shift, 0x35) == 0 && Jis(0, 0x36) == ',' && Jis(shift, 0x37) == '>' && Jis(shift, 0x38) == '?'); + Require(Jis(0, 0x87) == '\\' && Jis(shift, 0x87) == '_' && Jis(0, 0x89) == '\\' && Jis(shift, 0x89) == '|'); + Require(Jis(0, 0x59, KEYBOARD_LED_NUM_LOCK) == '1' && Jis(0, 0x59) == 0 && Jis(0, 0x55) == '*' && Jis(0, 0xe1) == 0); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, nullptr) == KEYBOARD_ERROR_INVALID_ARG); + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_106, 0, 0, 0x04, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, 2, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..96ffc1403 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -159,7 +159,7 @@ Throughout the project, every function at every stage either **does exactly what - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). -- [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. +- [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. From 98ed835123ea6c198f52939116e5d66d41d9bc70 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 6 Oct 2026 16:12:31 +0200 Subject: [PATCH 088/203] feat(agc): run COND_INDIRECT_BUFFER branches whose comparison always passes PPSA14632 (Unreal Engine) writes sceAgcCbBranch(mode 1, compare function 0, mask 0, reference 0) with a then buffer and a null else buffer of 0 dwords. It leaves the compare address uninitialised, so it is sometimes only 4-byte aligned. The writer rejected the null else buffer and the misaligned compare address, and the driver accepted opcode 0x3f only as the 4-dword INDIRECT_BUFFER, so the 14-dword branch would have failed at submission. sceAgcCbBranch no longer checks the compare address of an always-pass branch, which reads nothing, or the address of a buffer of 0 dwords. The driver runs the first buffer of an always-pass COND_INDIRECT_BUFFER in modes 1 and 2 (if-then and if-then-else) in place of the packet. A comparison, a predicated branch and other modes throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Cb/src/Branch.cpp | 6 +-- core/libs/prx/libSceAgc/tests/BranchPatch.cpp | 16 ++++++++ .../src/Driver/Queues/Submission.cpp | 17 +++++++++ core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 37 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgc/Cb/src/Branch.cpp b/core/libs/prx/libSceAgc/Cb/src/Branch.cpp index 649b810f6..b300fb533 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Branch.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Branch.cpp @@ -18,9 +18,9 @@ std::uint32_t* APS5_VABI sceAgcCbBranch(CommandBuffer* buf, std::uint8_t mode, s const auto address = reinterpret_cast(compareAddress); const auto first = reinterpret_cast(buffer1); const auto second = reinterpret_cast(buffer2); - Agc::Command::CheckAddress(address, 8, __func__); - Agc::Command::CheckAddress(first, 4, __func__); - Agc::Command::CheckAddress(second, 4, __func__); + if (compareFunction != 0) Agc::Command::CheckAddress(address, 8, __func__); + if (first != 0 || sizeInDwords1 != 0) Agc::Command::CheckAddress(first, 4, __func__); + if (second != 0 || sizeInDwords2 != 0) Agc::Command::CheckAddress(second, 4, __func__); return Agc::Command::Emit(buf, 0x3fu, {mode | (static_cast(compareFunction) << 8u), static_cast(address), static_cast(address >> 32u), static_cast(mask), static_cast(mask >> 32u), static_cast(reference), static_cast(reference >> 32u), static_cast(first), static_cast(first >> 32u), sizeInDwords1 | (static_cast(cachePolicy1) << 28u), static_cast(second), static_cast(second >> 32u), sizeInDwords2 | (static_cast(cachePolicy2) << 28u)}, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/BranchPatch.cpp b/core/libs/prx/libSceAgc/tests/BranchPatch.cpp index 0062068af..2382676ee 100644 --- a/core/libs/prx/libSceAgc/tests/BranchPatch.cpp +++ b/core/libs/prx/libSceAgc/tests/BranchPatch.cpp @@ -58,6 +58,21 @@ void testWriter() { check(storage.packet[expected.size()] == 0xabcdef01u, "branch command overwrote following word"); } +void testUnusedFields() { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + auto* packet = sceAgcCbBranch(&buffer, 1, 0, compareAt(0x0000001234567894ull), 0, 0, 0, targetAt(0x0000002233445564ull), 0x123u, 0, nullptr, 0); + const std::array expected{0xc00c3f00u, 0x1u, 0x34567894u, 0x12u, 0u, 0u, 0u, 0u, 0x33445564u, 0x22u, 0x123u, 0u, 0u, 0u}; + check(std::equal(expected.begin(), expected.end(), packet), "an always-taken branch without an else target was not written"); + packet = sceAgcCbBranch(&buffer, 1, 0, nullptr, 0, 0, 0, nullptr, 0, 0, nullptr, 0); + check(packet[1] == 1u && packet[2] == 0u && packet[8] == 0u && packet[10] == 0u, "an empty always-taken branch was not written"); + const auto cursor = buffer.cursor_up; + expectFailure([&] { sceAgcCbBranch(&buffer, 1, 3, compareAt(0x0000001234567894ull), 0, 0, 0, targetAt(0x2000u), 1, 0, nullptr, 0); }); + expectFailure([&] { sceAgcCbBranch(&buffer, 1, 0, nullptr, 0, 0, 0, nullptr, 1, 0, nullptr, 0); }); + expectFailure([&] { sceAgcCbBranch(&buffer, 2, 0, nullptr, 0, 0, 0, targetAt(0x2000u), 1, 0, nullptr, 1); }); + check(buffer.cursor_up == cursor, "a rejected branch advanced the command buffer"); +} + void testCompareAddress() { Storage storage; auto expected = storage.words; @@ -109,6 +124,7 @@ void testMalformedPackets() { int main() { try { testWriter(); + testUnusedFields(); testCompareAddress(); testTarget(sceAgcBranchPatchSetThenTarget, 8, 0x10000000u, "then target setter wrote the wrong words"); testTarget(sceAgcBranchPatchSetElseTarget, 11, 0x20000000u, "else target setter wrote the wrong words"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index cdc6d68b8..08f5cee6c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -43,6 +43,23 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std } reach(cursor, cursor + count); const auto opcode = (header >> 8u) & 0xffu; + if (opcode == 0x3fu && count == 14) { + const auto mode = guest[cursor + 1] & 3u; + require(mode == 1u || mode == 2u, "invalid COND_INDIRECT_BUFFER mode"); + require(((guest[cursor + 1] >> 8u) & 7u) == 0u, "a COND_INDIRECT_BUFFER with a comparison is not implemented"); + require(!Pm4::Predicated(header), "a predicated COND_INDIRECT_BUFFER is not implemented"); + const auto* target = reinterpret_cast(static_cast(guest[cursor + 8] & ~3u) | (static_cast(guest[cursor + 9] & 0xffffu) << 32u)); + const std::size_t targetWords = guest[cursor + 10] & 0xfffffu; + if (targetWords != 0) { + GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t)); + if (copySegment(submission, target, targetWords, budget)) { + require(guarded.empty(), "a REWIND inside a conditional execution range is not implemented"); + return true; + } + } + cursor += count; + continue; + } if (opcode == 0x3fu) { require(count == 4, "invalid INDIRECT_BUFFER size"); const auto* target = reinterpret_cast(static_cast(guest[cursor + 1] & ~3u) | (static_cast(guest[cursor + 2] & 0xffffu) << 32u)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 3af51bbd8..e8b1d74e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -737,6 +738,41 @@ void testConditionalSubmission() { check(results[13] == 0, "a rejected conditional submission executed a guarded packet"); } +std::vector branch(std::uint32_t mode, std::uint32_t function, const std::vector* first, const std::vector* second) { + const auto address = [](const std::vector* target) { return target ? reinterpret_cast(target->data()) : std::uintptr_t{0}; }; + const auto size = [](const std::vector* target) { return target ? static_cast(target->size()) : 0u; }; + return makePacket(0x3f, {mode | (function << 8u), 0, 0, 0, 0, 0, 0, static_cast(address(first)), static_cast(address(first) >> 32u), size(first), static_cast(address(second)), static_cast(address(second) >> 32u), size(second)}); +} + +void testBranchSubmission() { + static std::array results{}; + static std::vector first, second; + results.fill(0); + first = joinPackets({writeWord(results[0], 71)}); + second = joinPackets({writeWord(results[1], 72)}); + auto words = joinPackets({branch(1, 0, &first, nullptr), writeWord(results[2], 73)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + check(results[0] == 71 && results[1] == 0 && results[2] == 73, "an always-taken if-then COND_INDIRECT_BUFFER did not run its buffer"); + results.fill(0); + words = joinPackets({branch(2, 0, &first, &second), writeWord(results[2], 74)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + check(results[0] == 71 && results[1] == 0 && results[2] == 74, "an always-taken if-then-else COND_INDIRECT_BUFFER ran the wrong buffer"); + results.fill(0); + words = joinPackets({branch(1, 0, nullptr, nullptr), writeWord(results[2], 75)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + check(results[2] == 75, "an empty COND_INDIRECT_BUFFER skipped the next packet"); + for (const auto& [mode, function, text] : {std::tuple{1u, 3u, "with a comparison"}, std::tuple{0u, 0u, "invalid COND_INDIRECT_BUFFER mode"}}) { + results.fill(0); + words = joinPackets({writeWord(results[3], 1), branch(mode, function, &first, nullptr)}); + expectFailure([&] { submitWords(words); }, text); + AgcDriverWaitIdle_nid_postfix(); + check(results[0] == 0 && results[3] == 0, "a rejected COND_INDIRECT_BUFFER submission executed a packet"); + } +} + void testPredicatedSubmission() { alignas(16) std::uint64_t flag[2] = {0, 0}; alignas(16) std::array written{}; @@ -819,6 +855,7 @@ int main(int argc, char** argv) { testDriverSubmission(); testPredicatedSubmission(); testConditionalSubmission(); + testBranchSubmission(); LibcRunShutdown_nid_postfix(); std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..b712e7139 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -146,6 +146,7 @@ Throughout the project, every function at every stage either **does exactly what - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. +- A [COND_INDIRECT_BUFFER](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted, a positive denormal src2 counts as positive): the recompiler does not read MODE, so other IEEE/denormal settings are not modelled. - [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. From 6b6b93e26b212ebabafea39802f1407731aacfea Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 21:14:41 +0200 Subject: [PATCH 089/203] fix(agc): accept indirect register placeholders without an address Unreal Engine reserves SET_*_REG_INDIRECT packets with a null register list and no registers, and fills them in later with the sceAgcSet*RegIndirectPatch* functions. The writers rejected the null address. A null list is now accepted when it holds no registers, which reads nothing; a null list with registers still throws. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgc/Command/src/Packet.cpp | 2 +- .../prx/libSceAgc/tests/RegisterCommands.cpp | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index e2bf38b21..9a9a1827a 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -139,7 +139,7 @@ std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const std::uint32_t* WriteIndirectRegisters(CommandBuffer* buffer, std::uint32_t opcode, const volatile ShaderRegister* registers, std::uint32_t count, const char* function) { const auto address = reinterpret_cast(registers); - CheckAddress(address, 4, function); + if (address != 0 || count != 0) CheckAddress(address, 4, function); CheckBits(count, 0x3fffu, function); return Emit(buffer, opcode, {static_cast(address), static_cast(address >> 32u), 0x80000000u, count}, function); } diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index ffe1e8690..c5ddd3d07 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -21,6 +21,9 @@ std::uint32_t APS5_VABI sceAgcDcbSetUcRegistersIndirectGetSize(std::uint32_t); int APS5_VABI sceAgcSetCxRegIndirectPatchSetNumRegisters(std::uint32_t*, std::uint32_t); int APS5_VABI sceAgcSetShRegIndirectPatchSetNumRegisters(std::uint32_t*, std::uint32_t); int APS5_VABI sceAgcSetUcRegIndirectPatchSetNumRegisters(std::uint32_t*, std::uint32_t); +int APS5_VABI sceAgcSetCxRegIndirectPatchSetAddress(std::uint32_t*, const volatile ShaderRegister*); +int APS5_VABI sceAgcSetShRegIndirectPatchSetAddress(std::uint32_t*, const volatile ShaderRegister*); +int APS5_VABI sceAgcSetUcRegIndirectPatchSetAddress(std::uint32_t*, const volatile ShaderRegister*); std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer*, const volatile ShaderRegister*, std::uint32_t); std::uint32_t* APS5_VABI sceAgcCbSetUcRegistersDirect(CommandBuffer*, const volatile ShaderRegister*, std::uint32_t); std::uint32_t APS5_VABI sceAgcCbSetShRegistersDirectGetSize(std::uint32_t); @@ -105,6 +108,26 @@ void testIndirect() { } } +void testIndirectPlaceholder() { + const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; + const std::array addressSetters{sceAgcSetCxRegIndirectPatchSetAddress, sceAgcSetShRegIndirectPatchSetAddress, sceAgcSetUcRegIndirectPatchSetAddress}; + const std::array countSetters{sceAgcSetCxRegIndirectPatchSetNumRegisters, sceAgcSetShRegIndirectPatchSetNumRegisters, sceAgcSetUcRegIndirectPatchSetNumRegisters}; + const std::array headers{0xc0039f00u, 0xc0036300u, 0xc0036400u}; + const std::array registers{{{0x10u, 7}, {0x11u, 8}}}; + for (std::size_t i = 0; i < writers.size(); ++i) { + Storage storage; + auto* packet = writers[i](&storage.buffer, nullptr, 0); + check(packet[0] == headers[i] && packet[1] == 0 && packet[2] == 0 && packet[3] == 0x80000000u && packet[4] == 0, "placeholder packet mismatch"); + check(storage.buffer.cursor_up == packet + 5, "placeholder cursor mismatch"); + const auto before = storage.words; + expectFailure([&] { writers[i](&storage.buffer, nullptr, 1); }); + check(storage.words == before && storage.buffer.cursor_up == packet + 5, "null register list with a count modified the buffer"); + check(addressSetters[i](packet, registers.data()) == 0 && countSetters[i](packet, 2) == 0, "placeholder was not patched"); + const auto address = reinterpret_cast(registers.data()); + check(packet[1] == static_cast(address) && packet[2] == static_cast(address >> 32u) && packet[4] == 2, "patched placeholder mismatch"); + } +} + void testDirectList() { constexpr std::uint32_t sentinel = 0xabcdef01u; constexpr std::uint32_t wordSize = sizeof(std::uint32_t); @@ -158,6 +181,7 @@ int main() { try { testDirect(); testIndirect(); + testIndirectPlaceholder(); testDirectList(); std::puts("AGC register command tests passed"); return 0; From 16e76301ab04c05fba6588b577081172120f9d35 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 14:51:07 +0000 Subject: [PATCH 090/203] feat(libSceAgc): implement the WRITE_DATA destination and cache policy patches sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy, their Async forms and sceAgcAsyncWriteDataPatchSetAddressOrOffset threw. They now rewrite a WRITE_DATA packet in place: the destination in the graphics (sceAgcDcbWriteData) or compute (sceAgcAcbWriteData) encoding, the cache policy in bits 25-26 and the address in dwords 2-3, so the patched packet equals the one written with those values. A register destination is rejected when the packet requests write confirmation, as when the packet is written. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/CMakeLists.txt | 20 +++ .../prx/libSceAgc/Patch/src/WaitRegMem.cpp | 59 ++++++++- core/libs/prx/libSceAgc/Unimplemented.cpp | 25 ---- .../prx/libSceAgc/tests/WriteDataPatch.cpp | 123 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 198 insertions(+), 30 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index d3bb48f0b..463795856 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -265,6 +265,26 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_write_data_patch_tests EXCLUDE_FROM_ALL + tests/WriteDataPatch.cpp + Command/src/Packet.cpp + Command/src/Memory.cpp + Acb/src/Memory.cpp + DcbFlow/src/Memory.cpp + Patch/src/WaitRegMem.cpp +) +target_include_directories(agc_write_data_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_write_data_patch_tests PRIVATE libc) +configure_windows_unwind(agc_write_data_patch_tests) +if(BUILD_TESTING) + set_target_properties(agc_write_data_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_write_data_patch_tests COMMAND agc_write_data_patch_tests) + if(WIN32) + set_tests_properties(agc_write_data_patch_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_prime_utcl2_tests EXCLUDE_FROM_ALL tests/PrimeUtcl2.cpp Command/src/Packet.cpp diff --git a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp index daa5b4216..dfd55b511 100644 --- a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp @@ -7,6 +7,39 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void ValidateWriteData(const std::uint32_t* cmd, const char* function) { + Agc::Command::CheckAddress(reinterpret_cast(cmd), 4, function); + Agc::Command::Require(((cmd[0] >> 8u) & 0xffu) == 0x37u, function, "not a WRITE_DATA packet"); +} + +int SetWriteDataAddress(std::uint32_t* cmd, std::uint64_t address, const char* function) { + ValidateWriteData(cmd, function); + cmd[2] = static_cast(address); + cmd[3] = static_cast(address >> 32u); + return 0; +} + +int SetWriteDataDst(std::uint32_t* cmd, std::uint8_t dst, bool compute, const char* function) { + ValidateWriteData(cmd, function); + Agc::Command::CheckBits(dst, compute ? 0xfu : 0x1fu, function); + Agc::Command::Require(dst != 0 || (cmd[1] & (1u << 20u)) == 0, function, "register writes do not support write confirmation"); + const auto mask = compute ? 0xfu << 8u : (1u << 30u) | (0xfu << 8u); + const auto destination = compute ? static_cast(dst) << 8u : ((dst & 1u) << 30u) | ((dst & 0x1eu) << 7u); + cmd[1] = (cmd[1] & ~mask) | destination; + return 0; +} + +int SetWriteDataCachePolicy(std::uint32_t* cmd, std::uint8_t cachePolicy, const char* function) { + ValidateWriteData(cmd, function); + Agc::Command::CheckBits(cachePolicy, 3, function); + cmd[1] = (cmd[1] & ~(3u << 25u)) | (static_cast(cachePolicy) << 25u); + return 0; +} + +} + extern "C" { int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) { @@ -37,11 +70,27 @@ int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint } int APS5_VABI sceAgcWriteDataPatchSetAddressOrOffset(std::uint32_t* cmd, std::uint64_t address) { - Agc::Command::CheckAddress(reinterpret_cast(cmd), 4, __func__); - Agc::Command::Require(((cmd[0] >> 8u) & 0xffu) == 0x37u, __func__, "not a WRITE_DATA packet"); - cmd[2] = static_cast(address); - cmd[3] = static_cast(address >> 32u); - return 0; + return SetWriteDataAddress(cmd, address, __func__); +} + +int APS5_VABI sceAgcAsyncWriteDataPatchSetAddressOrOffset(std::uint32_t* cmd, std::uint64_t address) { + return SetWriteDataAddress(cmd, address, __func__); +} + +int APS5_VABI sceAgcWriteDataPatchSetDst(std::uint32_t* cmd, std::uint8_t dst) { + return SetWriteDataDst(cmd, dst, false, __func__); +} + +int APS5_VABI sceAgcAsyncWriteDataPatchSetDst(std::uint32_t* cmd, std::uint8_t dst) { + return SetWriteDataDst(cmd, dst, true, __func__); +} + +int APS5_VABI sceAgcWriteDataPatchSetCachePolicy(std::uint32_t* cmd, std::uint8_t cachePolicy) { + return SetWriteDataCachePolicy(cmd, cachePolicy, __func__); +} + +int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy(std::uint32_t* cmd, std::uint8_t cachePolicy) { + return SetWriteDataCachePolicy(cmd, cachePolicy, __func__); } } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 9c447d703..93842c4ff 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -81,11 +81,6 @@ int APS5_VABI sceAgcUnknown_7Wa3aeJgeVU() { return 0; } -int APS5_VABI sceAgcAsyncWriteDataPatchSetDst() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcGetGsPrimPayload() { NotImplemented_nid_no_patch(__func__); return 0; @@ -101,32 +96,12 @@ int APS5_VABI sceAgcGetShaderInstrumentation() { return 0; } -int APS5_VABI sceAgcAsyncWriteDataPatchSetAddressOrOffset() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcWriteDataPatchSetCachePolicy() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("rP5xLdOf26k", sceAgcUnknown_rP5xLdOf26k); int APS5_VABI sceAgcUnknown_rP5xLdOf26k() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcWriteDataPatchSetDst() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp b/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp new file mode 100644 index 000000000..2b2fae16e --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp @@ -0,0 +1,123 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); +std::uint32_t* APS5_VABI sceAgcAcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); +int APS5_VABI sceAgcWriteDataPatchSetAddressOrOffset(std::uint32_t*, std::uint64_t); +int APS5_VABI sceAgcAsyncWriteDataPatchSetAddressOrOffset(std::uint32_t*, std::uint64_t); +int APS5_VABI sceAgcWriteDataPatchSetDst(std::uint32_t*, std::uint8_t); +int APS5_VABI sceAgcAsyncWriteDataPatchSetDst(std::uint32_t*, std::uint8_t); +int APS5_VABI sceAgcWriteDataPatchSetCachePolicy(std::uint32_t*, std::uint8_t); +int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy(std::uint32_t*, std::uint8_t); +} + +namespace { + +constexpr std::array Payload{0x11111111u, 0x22222222u, 0x33333333u}; +constexpr std::uint64_t Address = 0x0000123456789ab0ull; +constexpr std::uint64_t OtherAddress = 0x0000fedcba987650ull; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action) { + try { + action(); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error("expected invalid input to fail"); +} + +struct Packet { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; +}; + +struct Fields { + std::uint8_t dst; + std::uint8_t cachePolicy; + std::uint64_t address; + std::uint8_t writeConfirm; +}; + +using Writer = std::uint32_t* (*)(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); +using AddressPatch = int (*)(std::uint32_t*, std::uint64_t); +using FieldPatch = int (*)(std::uint32_t*, std::uint8_t); + +struct Variant { + Writer writer; + AddressPatch setAddress; + FieldPatch setDst; + FieldPatch setCachePolicy; + std::uint8_t maxDst; +}; + +std::uint32_t* build(Packet& packet, const Variant& variant, const Fields& fields) { + return variant.writer(&packet.buffer, fields.dst, fields.cachePolicy, fields.address, Payload.data(), static_cast(Payload.size()), 1, fields.writeConfirm); +} + +void expectPatched(const Variant& variant, const Fields& from, const Fields& to, const char* message) { + Packet patched; + auto* packet = build(patched, variant, from); + if (from.address != to.address) check(variant.setAddress(packet, to.address) == 0, "address patch failed"); + if (from.dst != to.dst) check(variant.setDst(packet, to.dst) == 0, "destination patch failed"); + if (from.cachePolicy != to.cachePolicy) check(variant.setCachePolicy(packet, to.cachePolicy) == 0, "cache policy patch failed"); + Packet expected; + build(expected, variant, to); + check(patched.words == expected.words, message); +} + +void testVariant(const Variant& variant) { + for (std::uint8_t from = 1; from <= variant.maxDst; ++from) { + for (std::uint8_t to = 1; to <= variant.maxDst; ++to) { + expectPatched(variant, {from, 1, Address, 1}, {to, 1, Address, 1}, "destination patch does not match a packet built with that destination"); + } + } + expectPatched(variant, {5, 1, Address, 0}, {0, 1, Address, 0}, "register destination patch does not match a packet built with it"); + for (std::uint8_t from = 0; from < 4; ++from) { + for (std::uint8_t to = 0; to < 4; ++to) { + expectPatched(variant, {5, from, Address, 1}, {5, to, Address, 1}, "cache policy patch does not match a packet built with that policy"); + } + } + expectPatched(variant, {5, 2, Address, 1}, {5, 2, OtherAddress, 1}, "address patch does not match a packet built with that address"); + expectPatched(variant, {2, 0, Address, 1}, {variant.maxDst, 3, OtherAddress, 1}, "combined patches do not match a packet built with those fields"); + + Packet packet; + auto* written = build(packet, variant, {5, 1, Address, 1}); + const auto before = packet.words; + expectFailure([&] { variant.setDst(written, static_cast(variant.maxDst + 1u)); }); + expectFailure([&] { variant.setDst(written, 0); }); + expectFailure([&] { variant.setCachePolicy(written, 4); }); + check(packet.words == before, "rejected patch modified the packet"); + + std::array nop{0xc0021000u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}; + const auto nopBefore = nop; + expectFailure([&] { variant.setAddress(nop.data(), Address); }); + expectFailure([&] { variant.setDst(nop.data(), 5); }); + expectFailure([&] { variant.setCachePolicy(nop.data(), 1); }); + check(nop == nopBefore, "patch of another packet modified it"); +} + +} + +int main() { + try { + testVariant({sceAgcDcbWriteData, sceAgcWriteDataPatchSetAddressOrOffset, sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy, 0x1f}); + testVariant({sceAgcAcbWriteData, sceAgcAsyncWriteDataPatchSetAddressOrOffset, sceAgcAsyncWriteDataPatchSetDst, sceAgcAsyncWriteDataPatchSetCachePolicy, 0xf}); + std::puts("AGC write data patch tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..5ba7c34d7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -56,6 +56,7 @@ Throughout the project, every function at every stage either **does exactly what - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [qj7QZpgr9Uw](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `qj7QZpgr9Uw` writes; KytyPS5 returns 0 for an operation above 3, here it throws +- [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords - [fd5Bp5tGTgo](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name - [dolOmWH+huQ](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name - [dbOlWdppb4o](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware From c80d01cff45c5bcab456108b35fcc381523dd1c5 Mon Sep 17 00:00:00 2001 From: gookie <36005381+gookie-dev@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:02:06 +0200 Subject: [PATCH 091/203] feat(libScePad): declare the VR controller exports scePadVrControllerRead and scePadVrControllerGetDeviceInformation are imported by titles that open PSVR2 Sense controller port types. No library exported them, so eager binding failed at startup. Declare them as throwing stubs: the VR controller port types are not supported, and scePadOpen already rejects them. --- core/libs/prx/libScePad/Export.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index adc42a6b7..c00ae49bc 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -233,6 +233,16 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enab return PAD_OK; } +int APS5_VABI scePadVrControllerGetDeviceInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + APS5_EXPORT("fCWdlnmB1Ks", scePadUnknown_fCWdlnmB1Ks); int APS5_VABI scePadUnknown_fCWdlnmB1Ks(void) { NotImplemented_nid_no_patch(__func__); From b06278d96c2ca1e763e9da8637730682560a3bec Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:48:38 +0200 Subject: [PATCH 092/203] fix(audiodec): build libSceAudiodec.native from the libSceAudiodec sources Titles that link the native module (The Smurfs Dreams imports sceAudiodecClearContext and the other decoder exports from libSceAudiodec.native) found only the dummy export there. Both modules are now built from the same sources, as libSceFont and libSceAvPlayer already do for their native modules. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudiodec.native/CMakeLists.txt | 2 ++ core/libs/prx/libSceAudiodec.native/Export.cpp | 10 ---------- core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake | 14 ++++++++++++++ core/libs/prx/libSceAudiodec/CMakeLists.txt | 13 ++----------- 4 files changed, 18 insertions(+), 21 deletions(-) create mode 100644 core/libs/prx/libSceAudiodec.native/CMakeLists.txt delete mode 100644 core/libs/prx/libSceAudiodec.native/Export.cpp create mode 100644 core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake diff --git a/core/libs/prx/libSceAudiodec.native/CMakeLists.txt b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt new file mode 100644 index 000000000..a4e07a5c0 --- /dev/null +++ b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAudiodec/AudiodecLibrary.cmake) +add_sce_audiodec_library(libSceAudiodec.native) diff --git a/core/libs/prx/libSceAudiodec.native/Export.cpp b/core/libs/prx/libSceAudiodec.native/Export.cpp deleted file mode 100644 index fb34206c2..000000000 --- a/core/libs/prx/libSceAudiodec.native/Export.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -APS5_DUMMY_FUN - -} diff --git a/core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake b/core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake new file mode 100644 index 000000000..b806bb8c0 --- /dev/null +++ b/core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake @@ -0,0 +1,14 @@ +function(add_sce_audiodec_library target) + set(audiodecDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${audiodecDir}/Export.cpp + ${audiodecDir}/src/Codecs.cpp + ) + target_include_directories(${target} PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src + ) + target_link_libraries(${target} PRIVATE atrac9 ffmpeg libc libkernel) + set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceAudiodec/CMakeLists.txt b/core/libs/prx/libSceAudiodec/CMakeLists.txt index 0283a14a3..b851e4298 100644 --- a/core/libs/prx/libSceAudiodec/CMakeLists.txt +++ b/core/libs/prx/libSceAudiodec/CMakeLists.txt @@ -1,14 +1,5 @@ -add_library(libSceAudiodec SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Codecs.cpp -) -target_include_directories(libSceAudiodec PRIVATE - ${LIBS_INCLUDE_DIR} - ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src -) -target_link_libraries(libSceAudiodec PRIVATE atrac9 ffmpeg libc libkernel) -set_target_properties(libSceAudiodec PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(libSceAudiodec) +include(${CMAKE_CURRENT_SOURCE_DIR}/AudiodecLibrary.cmake) +add_sce_audiodec_library(libSceAudiodec) if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) From f6ce77bb7f4e7145f8ecfd7e2d5f44084a2aec35 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 15:49:37 +0000 Subject: [PATCH 093/203] feat(shader): accept texel offsets in 1D gathers image_gather4_lz_o on a 1D view threw. The 1D gather emulation picks the two texels around the coordinate itself, so the 6-bit x offset of the address is added to the left texel index before both texel centres are sampled, as the hardware offsets the footprint. Co-Authored-By: Claude Opus 5.5 --- .../execution/ImageGatherOneDimensional.cpp | 170 ++++++++++++++++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 13 +- 2 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp new file mode 100644 index 000000000..ac769d0b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 8; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t ClampWrap = 0; +constexpr std::uint32_t ClampEdge = 2; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array Gather1D{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0100, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; +alignas(256) constexpr std::array Gather1DOffset{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf15c0100, 0x00820602, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; + +struct Variant { + std::span code; + bool offset; + std::uint32_t clamp; + const char* name; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor() { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t clamp) { + return {clamp | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +std::uint8_t Red(std::uint32_t x) { + return static_cast(x * 31u + 5u); +} + +void FillTexels() { + Texels.fill(0); + const auto descriptor = TextureDescriptor(); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + const auto& mip = surface.mips.at(0); + for (std::uint32_t x = 0; x < Width; ++x) { + auto* texel = Texels.data() + mip.tiledOffset + x * 4u; + texel[0] = Red(x); + texel[1] = 0x11; + texel[2] = 0x22; + texel[3] = 0xff; + } +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto offset = static_cast(tid % 11u) - 5; + Input[tid * 4u + 0u] = (static_cast(offset) & 0x3fu) | (tid % 3u == 0u ? 0x3f00u : 0u); + Input[tid * 4u + 1u] = std::bit_cast(-0.3f + 0.05f * static_cast(tid)); + Input[tid * 4u + 2u] = 0u; + Input[tid * 4u + 3u] = 0u; + } +} + +std::uint32_t Texel(int x, std::uint32_t clamp) { + const int size = static_cast(Width); + const int address = clamp == ClampEdge ? std::clamp(x, 0, size - 1) : ((x % size) + size) % size; + return Red(static_cast(address)); +} + +std::array Expected(std::uint32_t tid, const Variant& variant) { + const float u = std::bit_cast(Input[tid * 4u + 1u]); + int left = static_cast(std::floor(u * static_cast(Width) - 0.5f)); + if (variant.offset) { + const auto field = Input[tid * 4u] & 0x3fu; + left += static_cast(field ^ 0x20u) - 0x20; + } + const auto first = Texel(left, variant.clamp); + const auto second = Texel(left + 1, variant.clamp); + return {first, second, second, first}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Variant& variant) { + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(); + const auto sampler = SamplerDescriptor(variant.clamp); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(variant.code.data()), std::as_bytes(variant.code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(variant.code.data()), variant.code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const Variant& variant) { + Output.fill(-1.0f); + const auto result = Compile(device, variant); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(variant.code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = Expected(tid, variant); + for (std::uint32_t component = 0; component < 4u; ++component) { + const float actual = Output[tid * 4u + component]; + const float scaled = actual * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && static_cast(std::lround(scaled)) == expected[component], std::string(variant.name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillInput(); + Run(*device, {Gather1D, false, ClampEdge, "1D gather, clamp to edge"}); + Run(*device, {Gather1D, false, ClampWrap, "1D gather, wrap"}); + Run(*device, {Gather1DOffset, true, ClampEdge, "1D gather with offsets, clamp to edge"}); + Run(*device, {Gather1DOffset, true, ClampWrap, "1D gather with offsets, wrap"}); + std::puts("image 1D gather tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 419bf78ed..a9c8da1e2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -538,7 +538,7 @@ std::uint32_t UnpackImageGather(SpirvValueEmitContext& ctx, const ImageEmitAcces return result; } -std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t coord) { +std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { auto& state = ctx.state; const auto numericClass = access.image.numericClass; state.module.EmitCapability(spv::CapabilityImageQuery); @@ -547,8 +547,11 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32(state), width, image, ConstantU32(state, 0)); const auto widthF32 = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToF, TypeF32(state), widthF32, width); - const auto left = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), left, GlslStd450(state), GLSLstd450Floor, Binary(state, spv::OpFSub, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), coord, widthF32), ConstantF32(state, 0x3f000000u))); + auto left = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), left, GlslStd450(state), GLSLstd450Floor, Binary(state, spv::OpFSub, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), setup.coord, widthF32), ConstantF32(state, 0x3f000000u))); + if (setup.layout.offset != NoImageComponent) { + left = Binary(state, spv::OpFAdd, TypeF32(state), left, Unary(state, spv::OpConvertSToF, TypeF32(state), PackedOffset(ctx, access, setup.layout))); + } const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); const auto vectorType = ImageVectorType(state, numericClass, 4); const auto scalarType = ImageScalarType(state, numericClass); @@ -810,10 +813,10 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } if (dimension == RdnaImageDimension::Dim1D) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagOffset) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } - const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup.coord); + const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true))); return; } From 30a6e03636d1c996a5bc7728823ed5ee0907ef12 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:58:35 +0200 Subject: [PATCH 094/203] fix(trophy2): accept a null data or details in sceNpTrophy2GetGameInfo The Smurfs Dreams (Unreal Engine) asks only for the game details and passes a null data pointer; on success it reads the trophy count from the details, on a negative result it takes its error path. The function already fills each output only when it is given, but rejected the call unless both were. Only a call without either output is rejected now. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp | 2 +- core/libs/tests/GuestNpTrophy2.cpp | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp index 6a41aa647..0c5b320f6 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp @@ -9,7 +9,7 @@ extern "C" { int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDetails* details, NpTrophy2GameData* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } (void)context; diff --git a/core/libs/tests/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index 5a286ab35..d52cc50ba 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -1,9 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include +#include extern "C" { int APS5_VABI sceNpTrophy2RegisterUnlockCallback(void*, void*); int APS5_VABI sceNpTrophy2UnregisterUnlockCallback(); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); } static void Require(bool value) { if (!value) std::abort(); } @@ -17,4 +20,17 @@ int main() { Require(sceNpTrophy2UnregisterUnlockCallback() == 0); Require(sceNpTrophy2RegisterUnlockCallback(reinterpret_cast(&OnUnlock), &userdata) == 0); Require(sceNpTrophy2UnregisterUnlockCallback() == 0); + + NpTrophy2GameDetails details{}; + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(1, 1, &details, nullptr) == 0); + Require(details.num_trophies != 0); + Require(sceNpTrophy2GetGameInfo(1, 1, nullptr, &data) == 0); + bool threw = false; + try { + sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); } From 05de11ac1e988c3adc2c41e72701491c96e30646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 18:01:00 +0200 Subject: [PATCH 095/203] feat(libSceAjm.native): implement sceAjmBatchJobGetInfo Co-Authored-By: Claude --- core/libs/prx/libSceAjm.native/Export.cpp | 8 ---- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 4 ++ core/libs/tests/GuestAjm.cpp | 48 ++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 62c0bc44e..dee862dbb 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -50,14 +50,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const return 0; } -int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* result) { - (void)info; - (void)instance; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 9b5542631..af9cd4d4e 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -1055,6 +1055,10 @@ int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instan return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_GAPLESS_DECODE, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandGaplessDecode)); } +int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* result) { + return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_FORMAT, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandFormat)); +} + int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance, void* result, size_t result_size) { return sceAjmBatchJobRunSplit(info, instance, RUN_GET_CODEC_INFO, nullptr, 0, nullptr, 0, result, result_size); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 0e9bb2483..434113f57 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -23,6 +23,7 @@ int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo*, std::uint32_t, std::uint64_t, con int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo*, std::uint32_t, const void*, int, void*); int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo*, std::uint32_t, void*, std::size_t); +int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); @@ -427,6 +428,52 @@ void TestCodecInfo(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +struct FormatSideband { + std::int32_t result; + std::int32_t internalResult; + std::uint32_t numChannels; + std::uint32_t channelMask; + std::uint32_t sampleRate; + std::uint32_t sampleEncoding; + std::uint32_t bitrate; + std::uint32_t reserved; +}; + +void TestGetInfo(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + FormatSideband early{-1, -1, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &early) == 0); + Submit(context, info); + Require(early.result == 1 && early.numChannels == 0xaaaaaaaau && early.sampleRate == 0xaaaaaaaau); + + const std::uint8_t config[8] = {0xFE, 0x72, 0x1F, 0xF0}; + std::int32_t initResult[2] = {-1, -1}; + FormatSideband at9{-1, -1, 0, 0, 0, 0xaaaaaaaau, 0, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, config, sizeof(config), initResult) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &at9) == 0); + Submit(context, info); + Require(initResult[0] == 0); + Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.reserved == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); + std::vector pcm(1152); + DecodeSideband decoded{}; + FormatSideband mp3{-1, -1, 0, 0, 0, 0xaaaaaaaau, 0, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecodeSingle(&info, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &mp3) == 0); + Submit(context, info); + Require(decoded.result == 0 && decoded.inputConsumed == 96); + Require(mp3.result == 0 && mp3.internalResult == 0 && mp3.numChannels == 1 && mp3.channelMask == 0x4 && mp3.sampleRate == 48000 && mp3.sampleEncoding == 0 && mp3.bitrate == 32000 && mp3.reserved == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestBatchWaitRelease(std::uint32_t context) { constexpr int invalidBatch = static_cast(0x80930004); std::vector batch(64); @@ -487,6 +534,7 @@ int main() { TestDecodeSingle(context); TestGaplessDecode(context); TestCodecInfo(context); + TestGetInfo(context); TestBatchWaitRelease(context); TestBatchCancel(context); Require(sceAjmFinalize(context) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..a8af3b547 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -103,6 +103,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture +- [sceAjmBatchJobGetInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted From 42ab8af70e645ff0cb6d6c26d623a902c7953db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 18:20:52 +0200 Subject: [PATCH 096/203] feat(shader): implement s_bfe_i64 Co-Authored-By: Claude --- .../tests/execution/ScalarSop2.cpp | 92 +++++++++---------- .../include/RdnaDecoder/RdnaOpcode.hpp | 1 + .../recompiler/RdnaDecoder/src/RdnaOpcode.cpp | 1 + .../RdnaDecoder/src/RdnaScalarOpDecoder.cpp | 2 + .../Translation/TranslationContext.hpp | 2 +- .../Translation/src/IntegerInstructions.cpp | 14 ++- .../Translation/src/ScalarInstructions.cpp | 4 +- 7 files changed, 65 insertions(+), 51 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp index cec18794d..ed6a6bbe0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp @@ -23,7 +23,7 @@ constexpr std::uint32_t Results = 64; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34060088, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xbe940380, 0xd7600008, 0x02002904, 0xd7600009, 0x02002905, 0xd760000a, 0x02002906, 0xd760000b, 0x02002907, 0xbe9e0380, 0x871f810b, 0x828c0a08, 0x821e1e1e, 0xd761000a, 0x0200280c, 0x871f810b, 0x858c0a08, 0x821e1e1e, 0xd761000b, @@ -51,22 +51,22 @@ alignas(256) constexpr std::array Code{ 0xd761003e, 0x0200280c, 0xbf060b0b, 0x990c0a08, 0x821e1e1e, 0xd761003f, 0x0200280c, 0xbf060b0b, 0x998c0a08, 0x821e1e1e, 0xd7610040, 0x0200280c, 0xbf060b0b, 0x9a0c0a08, 0x821e1e1e, 0xd7610041, 0x0200280c, 0xbf060b0b, 0x9a8c0a08, 0x821e1e1e, 0xd7610042, 0x0200280c, 0xbf060b0b, 0x9b0c0a08, - 0x821e1e1e, 0xd7610043, 0x0200280c, 0xd7610049, 0x0200281e, 0x80148114, 0xbf0aa014, 0xbf85ff26, - 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, - 0xe0781040, 0x80011a03, 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, - 0xe0781080, 0x80012a03, 0xe0781090, 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, - 0xe07810c0, 0x80013a03, 0xe07810d0, 0x80013e03, 0xe07810e0, 0x80014203, 0xe07810f0, 0x80014603, - 0xbf810000, + 0x821e1e1e, 0xd7610043, 0x0200280c, 0x950c0a08, 0x821e1e1e, 0xd7610044, 0x0200280c, 0xd7610045, + 0x0200280d, 0xd7610049, 0x0200281e, 0x80148114, 0xbf0aa014, 0xbf85ff20, 0xe0781000, 0x80010a03, + 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xe0781040, 0x80011a03, + 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, 0xe0781080, 0x80012a03, + 0xe0781090, 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, 0xe07810c0, 0x80013a03, + 0xe07810d0, 0x80013e03, 0xe07810e0, 0x80014203, 0xe07810f0, 0x80014603, 0xbf810000, }; constexpr std::uint32_t Rows[32][4] = { {0xbde5c099u, 0x00000001u, 0xc6a53877u, 0xa6233255u}, {0x00000000u, 0x4e5a4cafu, 0x00000000u, 0xd86f17ecu}, - {0x00410003u, 0x80000000u, 0xffffffffu, 0xffff8000u}, - {0x80000000u, 0x00000040u, 0x7fffffffu, 0x0010001cu}, - {0xe05e9899u, 0xb77701e2u, 0x007f0000u, 0x0000001fu}, - {0x7fffffffu, 0xb89c4e56u, 0xff297d0eu, 0xffffffffu}, + {0xfffffffeu, 0x80000000u, 0x00400000u, 0x9abcdef0u}, + {0x00000000u, 0x40000000u, 0x98a5c920u, 0x40000000u}, + {0xfffffffeu, 0xffffffffu, 0x7fffffffu, 0x00200000u}, {0x4b78dc3du, 0x0000003fu, 0x741b324du, 0xb057c162u}, + {0xfffffffeu, 0x80000000u, 0x00000001u, 0x8a887204u}, {0x00008000u, 0x40000000u, 0x61394c10u, 0x0000003fu}, {0xffffffffu, 0x40000000u, 0x7fffffffu, 0x66868695u}, {0x0000003fu, 0x40000000u, 0x617959ceu, 0xffff8000u}, @@ -81,6 +81,7 @@ constexpr std::uint32_t Rows[32][4] = { {0xf129c8c6u, 0x10000000u, 0x00080004u, 0x1ba95a54u}, {0x0001001fu, 0x0960afe9u, 0x7fffffffu, 0x10000000u}, {0xeb07c30du, 0x0010001cu, 0x8b8e8f4eu, 0x00400000u}, + {0x7fffffffu, 0xb89c4e56u, 0xff297d0eu, 0xffffffffu}, {0x007f0000u, 0x0020003fu, 0x74a677c6u, 0x2547f19cu}, {0x00000002u, 0x558d2adbu, 0x216d27a2u, 0x90656c8fu}, {0xa161d909u, 0x69f0441eu, 0x0040003fu, 0xffffffffu}, @@ -90,44 +91,43 @@ constexpr std::uint32_t Rows[32][4] = { {0x43cdc356u, 0xb0ddb48bu, 0xf4f6717cu, 0xa6523995u}, {0x00000000u, 0x00080004u, 0x0010001cu, 0x10000000u}, {0xcc3136d8u, 0x0010001cu, 0xbce240c6u, 0x68916d08u}, - {0x00000040u, 0xb036d9ddu, 0x0001001fu, 0x84a4c93fu}, - {0x0000003fu, 0x0001001fu, 0x7a2c54a0u, 0x7fffffffu} + {0x00000040u, 0xb036d9ddu, 0x0001001fu, 0x84a4c93fu} }; constexpr std::uint32_t Expected[32][64] = { - {0xf7408821u, 0xbde5c099u, 0x00000001u, 0x848af910u, 0xf7408822u, 0xbde5c099u, 0xbde5c099u, 0xc6a53877u, 0xc6a53877u, 0x7b40f8eeu, 0x3940c088u, 0xbdffc799u, 0x001a0700u, 0x84bf0711u, 0x00000000u, 0x0000199fu, 0xffe5f8ffu, 0xa6233255u, 0x7b40f8eeu, 0xa6233254u, 0x3940c088u, 0x00000000u, 0xbdffc799u, 0x59dccdabu, 0x7b5affeeu, 0xfffffffeu, 0x001a0700u, 0x59dccdaau, 0x84bf0711u, 0x59dccdabu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4c800000u, 0x0000017bu, 0x00000000u, 0x00000000u, 0xffffff7bu, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0xff800000u, 0xffffff7bu, 0x0000017bu, 0x00000000u, 0x00000000u, 0x0000035eu, 0x08bf77deu, 0x4270b9a9u, 0xbe3c3adbu, 0xb5d33d3fu, 0xa5014207u, 0x3877c099u, 0xc6a5c099u, 0xc6a5bde5u, 0x935a41eeu, 0x0ecf48deu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x00000000u, 0x00000000u, 0xd86f17ecu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000007u, 0x00000000u, 0xde7f5fefu, 0x00000000u, 0x96355b43u, 0x00000000u, 0x06104803u, 0xffffffffu, 0x6fdaecbfu, 0xffffffffu, 0xb7b5fb53u, 0xffffffffu, 0x2180a010u, 0xffffffffu, 0x69caa4bcu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000001fu}, - {0x00410004u, 0xffffffffu, 0xffff8000u, 0x00410002u, 0x00410004u, 0xffffffffu, 0x00410003u, 0x00410003u, 0xffffffffu, 0xffbefffcu, 0x00000000u, 0x00410003u, 0x00000000u, 0x00410003u, 0x00000000u, 0x000010d5u, 0xffffffffu, 0xffff8000u, 0xffbefffcu, 0x7fff8000u, 0x00000000u, 0x00000000u, 0x00410003u, 0x80007fffu, 0xffbefffcu, 0x7fffffffu, 0x00000000u, 0x00007fffu, 0x00410003u, 0x80007fffu, 0x00000000u, 0x0000006fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x000002b9u, 0x00410004u, 0x00820005u, 0x0104000bu, 0x02080017u, 0x0410002fu, 0xffff0003u, 0xffff0003u, 0xffff0041u, 0x00410002u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x00000001u, 0x7fffffffu, 0x0010001cu, 0xffffffffu, 0x00000001u, 0x80000000u, 0x7fffffffu, 0x7fffffffu, 0x80000000u, 0xffffffffu, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000033cu, 0xffffffffu, 0x0010005cu, 0xffffffffu, 0x0010005cu, 0x80000000u, 0x00000040u, 0x80000000u, 0xffefffe3u, 0xffffffffu, 0xffffffffu, 0x00000000u, 0xffefffa3u, 0x00000000u, 0xffefffa3u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000015eu, 0x00000001u, 0x7fffffffu, 0x7fffffffu, 0x7fffffffu, 0x7fffffffu, 0xffff0000u, 0x7fff0000u, 0x7fff8000u, 0x3fffffffu, 0xc0000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xdfdf9898u, 0xe05e9899u, 0xb77701e2u, 0xe0dd9899u, 0xdfdf9899u, 0xe05e9899u, 0x007f0000u, 0x007f0000u, 0xe05e9899u, 0xe0219899u, 0xe0009899u, 0xffdeffffu, 0x1f806766u, 0x1fde6766u, 0x00000000u, 0x0000093fu, 0xe07f9899u, 0xb77701ffu, 0xe0219899u, 0xb77701fdu, 0xe0009899u, 0xb77701e0u, 0xffdeffffu, 0xffffffe2u, 0xffa1ffffu, 0xfffffffdu, 0x1f806766u, 0x4888fe00u, 0x1fde6766u, 0x4888fe02u, 0x00000000u, 0x0000007fu, 0xe05e9899u, 0xb77701e2u, 0xe05e9899u, 0xe05e9899u, 0xb77701e2u, 0xe05e9899u, 0xe05e9899u, 0xb77701e2u, 0x01ffffffu, 0x01ffffffu, 0x00000000u, 0xe05e9899u, 0xe05e9899u, 0xe05e9899u, 0xb77701e2u, 0x000003ffu, 0x20206767u, 0xc13c3132u, 0x81f96264u, 0x0373c4c8u, 0x06688990u, 0x00009899u, 0x007f9899u, 0x007fe05eu, 0x006f4eedu, 0xfff04eedu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x80d682f0u, 0x7fffffffu, 0xb89c4e56u, 0x7f297d0du, 0x80d682f1u, 0xff297d0eu, 0x7fffffffu, 0x7fffffffu, 0xff297d0eu, 0x80d682f1u, 0x00d682f1u, 0x7fffffffu, 0x00000000u, 0x7f297d0eu, 0x00000000u, 0x00001addu, 0xffffffffu, 0xffffffffu, 0x80d682f1u, 0x4763b1a9u, 0x00d682f1u, 0x00000000u, 0x7fffffffu, 0xb89c4e56u, 0x80d682f1u, 0x4763b1a9u, 0x00000000u, 0x00000000u, 0x7f297d0eu, 0xb89c4e56u, 0x00000000u, 0x0000007du, 0xffffc000u, 0x13959fffu, 0x0001ffffu, 0x3959ffffu, 0x0002e271u, 0x0001ffffu, 0x3959ffffu, 0xfffee271u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x0001ffffu, 0x0001ffffu, 0x3959ffffu, 0x00000071u, 0x000003ffu, 0x7f297d0fu, 0xff297d0cu, 0xff297d0au, 0xff297d06u, 0xff297cfeu, 0x7d0effffu, 0xff29ffffu, 0xff297fffu, 0x7f94be86u, 0xff94be87u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xd75da9f0u, 0x741b324du, 0xb057c162u, 0xbf940e8au, 0xd75da9f0u, 0x4b78dc3du, 0x4b78dc3du, 0x741b324du, 0x741b324du, 0x3f63ee70u, 0x0b60cc30u, 0xcbfcddbfu, 0x80840182u, 0xc09c118fu, 0x00000000u, 0x0000159fu, 0x7f7bfe7du, 0xb057c17fu, 0x3f63ee70u, 0xb057c15du, 0x0b60cc30u, 0x0000001du, 0xcbfcddbfu, 0x4fa83ebfu, 0xbfe7eff2u, 0xffffffddu, 0x80840182u, 0x4fa83e80u, 0xc09c118fu, 0x4fa83ea2u, 0x00000000u, 0x0000007fu, 0x1b87a000u, 0x0007e96fu, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0xffffe000u, 0xffffe000u, 0xffffffffu, 0x00025bc6u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x000003ffu, 0x28a25610u, 0x0b0ceac7u, 0xa1fea341u, 0xcfe21435u, 0x2ba8f61du, 0x324ddc3du, 0x741bdc3du, 0x741b4b78u, 0x223ac85fu, 0x223ac85fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x9ec733efu, 0x00008000u, 0x40000000u, 0x6139cc10u, 0x9ec733f0u, 0x00008000u, 0x00008000u, 0x61394c10u, 0x61394c10u, 0x6139cc10u, 0x00008000u, 0x9ec6b3efu, 0x9ec633efu, 0x9ec633efu, 0x00000000u, 0x0000199fu, 0x6139cc10u, 0x4000003fu, 0x6139cc10u, 0x4000003fu, 0x00008000u, 0x40000000u, 0x9ec6b3efu, 0xffffffc0u, 0xffffffffu, 0xffffffffu, 0x9ec633efu, 0xbfffffc0u, 0x9ec633efu, 0xbfffffc0u, 0x00000000u, 0x0000007fu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x000002b9u, 0x6138cc10u, 0x613a4c10u, 0x613b4c10u, 0x613d4c10u, 0x61414c10u, 0x4c108000u, 0x61398000u, 0x61390000u, 0x0000309cu, 0x0000309cu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x7fffffffu, 0xffffffffu, 0x40000000u, 0x7ffffffeu, 0x80000000u, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x80000000u, 0x80000000u, 0xffffffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x0000093du, 0xffffffffu, 0x66868695u, 0x80000000u, 0x26868695u, 0x80000000u, 0x00000000u, 0xffffffffu, 0xd979796au, 0x80000000u, 0xbfffffffu, 0x00000000u, 0x9979796au, 0x7fffffffu, 0xd979796au, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x80000000u, 0x7ffffffdu, 0x7ffffffbu, 0x7ffffff7u, 0x7fffffefu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7ffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x9e86a671u, 0x617959ceu, 0xffff8000u, 0x61795a0du, 0x9e86a671u, 0x0000003fu, 0x0000003fu, 0x617959ceu, 0x617959ceu, 0x617959f1u, 0x00000031u, 0x9e86a63fu, 0x9e86a600u, 0x9e86a60eu, 0x00000000u, 0x0000119fu, 0x617959ffu, 0xffff8000u, 0x617959f1u, 0xbfff8000u, 0x00000031u, 0x00000000u, 0x9e86a63fu, 0x40007fffu, 0xfffffff1u, 0xbfffffffu, 0x9e86a600u, 0x00007fffu, 0x9e86a60eu, 0x40007fffu, 0x00000000u, 0x0000007fu, 0x000fc000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00010000u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x000002b9u, 0x6179598fu, 0x61795a4cu, 0x61795acau, 0x61795bc6u, 0x61795dbeu, 0x59ce003fu, 0x6179003fu, 0x61790000u, 0x00000017u, 0x00000017u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x3bfa6abbu, 0xc4069545u, 0x80000000u, 0xc4079545u, 0x3bfa6abbu, 0xc4069545u, 0x00010000u, 0x00010000u, 0xc4069545u, 0xc4079545u, 0x00010000u, 0x3bf96abau, 0x3bf86abau, 0x3bf86abau, 0x00000000u, 0x000010dfu, 0xc4079545u, 0x80000040u, 0xc4079545u, 0x80000040u, 0x00010000u, 0x00000040u, 0x3bf96abau, 0x7fffffffu, 0xffffffffu, 0xffffffffu, 0x3bf86abau, 0x7fffffbfu, 0x3bf86abau, 0x7fffffbfu, 0x00000000u, 0x0000007fu, 0x00200000u, 0x00000800u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x3bfa6abbu, 0xc4089545u, 0xc40a9545u, 0xc40e9545u, 0xc4169545u, 0x95450000u, 0xc4060000u, 0xc4060001u, 0x0000c406u, 0xffffc406u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x10d822c6u, 0x015c33b2u, 0x00010000u, 0x13908a2au, 0x10d822c6u, 0x015c33b2u, 0x015c33b2u, 0x12345678u, 0x12345678u, 0x136865cau, 0x12204448u, 0xfeb7de7du, 0xec838805u, 0xec979a35u, 0x00000000u, 0x0000007fu, 0x137c77fau, 0x00010000u, 0x136865cau, 0x00010000u, 0x12204448u, 0x00000000u, 0xfeb7de7du, 0xfffeffffu, 0xffebedcfu, 0xffffffffu, 0xec838805u, 0xfffeffffu, 0xec979a35u, 0xfffeffffu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x59e00000u, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000048du, 0x00000000u, 0x00000000u, 0xfffc0000u, 0x00000000u, 0xfffc0000u, 0x0000048du, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000035eu, 0x10d822c6u, 0x25c4e0a2u, 0x4a2d8d92u, 0x92fee772u, 0x24a19b32u, 0x33b25678u, 0x015c5678u, 0x015c1234u, 0x0018c2d2u, 0x0018c2d2u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000023fu}, - {0x2a74915au, 0x0000ffffu, 0x00010000u, 0x2a769158u, 0x2a74915au, 0x0000ffffu, 0x0000ffffu, 0x2a759159u, 0x2a759159u, 0x2a756ea6u, 0x2a750000u, 0xffff9159u, 0xd58a0000u, 0xd58a9159u, 0x00000000u, 0x0000007fu, 0x2a75ffffu, 0x4a0fe75du, 0x2a756ea6u, 0x4a0ee75du, 0x2a750000u, 0x4a0ee75du, 0xffff9159u, 0xffffffffu, 0xffff6ea6u, 0xfffeffffu, 0xd58a0000u, 0xb5f018a2u, 0xd58a9159u, 0xb5f118a2u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x2a74915au, 0x54ec22b1u, 0xa9d74563u, 0x53ad8ac7u, 0xa75a158fu, 0xffff9159u, 0x00009159u, 0x00002a75u, 0x00002a75u, 0x00002a75u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000027fu}, - {0x0000fffdu, 0x0000ffffu, 0xe4cc4132u, 0x00010000u, 0x0000fffeu, 0x00000001u, 0x00000001u, 0x0000ffffu, 0x0000ffffu, 0x0000fffeu, 0x0000fffeu, 0xffffffffu, 0xffff0000u, 0xffff0001u, 0x00000000u, 0x0000087fu, 0x0000ffffu, 0xe4cc413fu, 0x0000fffeu, 0xe4cc410du, 0x0000fffeu, 0xe4cc4100u, 0xffffffffu, 0xfffffff2u, 0xfffffffeu, 0xffffffcdu, 0xffff0000u, 0x1b33bec0u, 0xffff0001u, 0x1b33bef2u, 0x00000000u, 0x0000007fu, 0x0001fffeu, 0xc9988264u, 0x00007fffu, 0x00007fffu, 0x72662099u, 0x00007fffu, 0x00007fffu, 0xf2662099u, 0xfffffffeu, 0xfffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0000fffeu, 0x0001ffffu, 0x0003fffdu, 0x0007fff9u, 0x000ffff1u, 0x0001ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xad7bd36cu, 0x43892dfcu, 0x00000005u, 0xd996888bu, 0xad7bd36du, 0x960d5a8fu, 0x43892dfcu, 0x43892dfcu, 0x960d5a8fu, 0xd5847773u, 0x41802570u, 0x6bfbadfcu, 0x28728000u, 0x2a7b888cu, 0x00000000u, 0x00001adfu, 0xd78d7fffu, 0x0021001fu, 0xd5847773u, 0x0021001au, 0x41802570u, 0x00000000u, 0x6bfbadfcu, 0xffdeffe5u, 0xfdf6f773u, 0xfffffffau, 0x28728000u, 0xffdeffe0u, 0x2a7b888cu, 0xffdeffe5u, 0x00000000u, 0x0000007fu, 0x96fe0000u, 0x0002a1c4u, 0x00008712u, 0x000a8712u, 0x00000000u, 0x00008712u, 0x000a8712u, 0x00000000u, 0xffff8000u, 0xffff8000u, 0xffffffffu, 0x00000712u, 0x00000712u, 0x00000712u, 0x00000000u, 0x000003ffu, 0x52842c93u, 0x1d1fb687u, 0xa432127fu, 0xb256ca6fu, 0xcea03a4fu, 0x5a8f2dfcu, 0x960d2dfcu, 0x960d4389u, 0x2795e6ccu, 0xe40cb8d0u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xd0d2b3b9u, 0x2f4d4c86u, 0x00000040u, 0x2f6d4cc5u, 0xd0d2b3b9u, 0x0020003fu, 0x0020003fu, 0x2f4d4c86u, 0x2f4d4c86u, 0x2f6d4cb9u, 0x00200039u, 0xd0b2b37fu, 0xd092b340u, 0xd092b346u, 0x00000000u, 0x0000119fu, 0x2f6d4cbfu, 0x7b121dc5u, 0x2f6d4cb9u, 0x7b121d85u, 0x00200039u, 0x7b121d85u, 0xd0b2b37fu, 0xffffffffu, 0xfffffff9u, 0xffffffbfu, 0xd092b340u, 0x84ede23au, 0xd092b346u, 0x84ede27au, 0x00000000u, 0x0000007fu, 0x08000fc0u, 0xc4877140u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0xffffffc0u, 0xffffffc0u, 0xffffffffu, 0x00008000u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x000003ffu, 0x2f2d4c47u, 0x2f8d4d04u, 0x2fcd4d82u, 0x304d4e7eu, 0x314d5076u, 0x4c86003fu, 0x2f4d003fu, 0x2f4d0020u, 0x0005e9b5u, 0x0005e9b5u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xfb5fed18u, 0x00000001u, 0x5b8e8fb2u, 0x04a012e9u, 0xfb5fed19u, 0x00000001u, 0x00000001u, 0x04a012e8u, 0x04a012e8u, 0x04a012e9u, 0x00000001u, 0xfb5fed17u, 0xfb5fed16u, 0xfb5fed16u, 0x00000000u, 0x0000199fu, 0x04a012e9u, 0x5bcf8fb3u, 0x04a012e9u, 0x5bcf8fb1u, 0x00000001u, 0x5b8e8fb0u, 0xfb5fed17u, 0xffbefffeu, 0xffffffffu, 0xfffffffdu, 0xfb5fed16u, 0xa430704cu, 0xfb5fed16u, 0xa430704eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x000002b9u, 0x04a012e7u, 0x04a012eau, 0x04a012ecu, 0x04a012f0u, 0x04a012f8u, 0x12e80001u, 0x04a00001u, 0x04a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x0baccdb2u, 0xf453324eu, 0xffff8000u, 0xf453324eu, 0x0baccdb2u, 0xf453324eu, 0x00000000u, 0x00000000u, 0xf453324eu, 0xf453324eu, 0x00000000u, 0x0baccdb1u, 0x0baccdb1u, 0x0baccdb1u, 0x00000000u, 0x000010d7u, 0xf453324eu, 0xffff8040u, 0xf453324eu, 0xffff8040u, 0x00000000u, 0x00000040u, 0x0baccdb1u, 0x00007fffu, 0xffffffffu, 0xffffffffu, 0x0baccdb1u, 0x00007fbfu, 0x0baccdb1u, 0x00007fbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00100000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x000002b9u, 0x0baccdb2u, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0x324e0000u, 0xf4530000u, 0xf4530000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xf121c8c2u, 0x00080004u, 0x1ba95a54u, 0xf131c8cau, 0xf121c8c2u, 0xf129c8c6u, 0x00080004u, 0x00080004u, 0xf129c8c6u, 0xf121c8c2u, 0xf121c8c2u, 0xffffffffu, 0x0ed63739u, 0x0ede373du, 0x00000000u, 0x0000013fu, 0xf129c8c6u, 0x1ba95a54u, 0xf121c8c2u, 0x0ba95a54u, 0xf121c8c2u, 0x00000000u, 0xffffffffu, 0xf456a5abu, 0xfff7fffbu, 0xefffffffu, 0x0ed63739u, 0xe456a5abu, 0x0ede373du, 0xf456a5abu, 0x00000000u, 0x0000007fu, 0x129c8c60u, 0x0000000fu, 0x0f129c8cu, 0x0f129c8cu, 0x01000000u, 0xff129c8cu, 0x0f129c8cu, 0x01000000u, 0x000003f0u, 0x000003f0u, 0x00000000u, 0xffffff8cu, 0x0000008cu, 0x0000008cu, 0x00000000u, 0x000003ffu, 0x0ede373eu, 0xe25b9190u, 0xc4af231cu, 0x89564634u, 0x12a48c64u, 0x0004c8c6u, 0x0008c8c6u, 0x0008f129u, 0x00078952u, 0xffff894eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x80010020u, 0x7fffffffu, 0x10000000u, 0x8001001eu, 0x80010020u, 0x0001001fu, 0x0001001fu, 0x7fffffffu, 0x7fffffffu, 0x7ffeffe0u, 0x00000000u, 0x8001001fu, 0x80000000u, 0x8001001fu, 0x00000000u, 0x00001597u, 0x7fffffffu, 0x1960afe9u, 0x7ffeffe0u, 0x1960afe9u, 0x00000000u, 0x0960afe9u, 0x8001001fu, 0xefffffffu, 0xfffeffe0u, 0xffffffffu, 0x80000000u, 0xe69f5016u, 0x8001001fu, 0xe69f5016u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x7ffeffe0u, 0x8002003du, 0x8004007bu, 0x800800f7u, 0x801001efu, 0xffff001fu, 0x7fff001fu, 0x7fff0001u, 0x0000800fu, 0x0000800fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x5f7933bfu, 0x8b8e8f4eu, 0x00400000u, 0x7696525bu, 0x5f7933bfu, 0x8b8e8f4eu, 0x8b8e8f4eu, 0xeb07c30du, 0xeb07c30du, 0x60894c43u, 0x60014001u, 0xff77f3bdu, 0x147030b0u, 0x9f76b3bcu, 0x00000000u, 0x0000047fu, 0xeb8fcf4fu, 0x0050001cu, 0x60894c43u, 0x0050001cu, 0x60014001u, 0x0010001cu, 0xff77f3bdu, 0xffbfffffu, 0x74f97cf3u, 0xffffffffu, 0x147030b0u, 0xffafffe3u, 0x9f76b3bcu, 0xffafffe3u, 0x00000000u, 0x0000007fu, 0xf0c34000u, 0x00073ac1u, 0x0003ac1fu, 0x0073ac1fu, 0x00000040u, 0xffffac1fu, 0x0073ac1fu, 0x00000040u, 0x07ffc000u, 0x07ffc000u, 0x00000000u, 0xffffec1fu, 0x00002c1fu, 0x00002c1fu, 0x00000000u, 0x000003ffu, 0x5f7933bfu, 0x619e1568u, 0x37ad9b82u, 0xe3cca7b6u, 0x3c0ac01eu, 0x8f4ec30du, 0x8b8ec30du, 0x8b8eeb07u, 0x802018c7u, 0x0989c66cu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x8bd8883au, 0x74a677c6u, 0x2547f19cu, 0x752577c6u, 0x8bd8883au, 0x007f0000u, 0x007f0000u, 0x74a677c6u, 0x74a677c6u, 0x74d977c6u, 0x00590000u, 0x8b7f8839u, 0x8b008839u, 0x8b268839u, 0x00000000u, 0x0000119fu, 0x74ff77c6u, 0x2567f1bfu, 0x74d977c6u, 0x2567f1a3u, 0x00590000u, 0x00200023u, 0x8b7f8839u, 0xdab80e7fu, 0xffd9ffffu, 0xffffffe3u, 0x8b008839u, 0xda980e40u, 0x8b268839u, 0xda980e5cu, 0x00000000u, 0x0000007fu, 0x1fc00000u, 0x08000fc0u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0001fc00u, 0x0001fc00u, 0xfc01fc00u, 0x00000000u, 0x000003ffu, 0x742777c6u, 0x75a477c6u, 0x76a277c6u, 0x789e77c6u, 0x7c9677c6u, 0x77c60000u, 0x74a60000u, 0x74a6007fu, 0x0039de95u, 0x0039de95u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xde92d85fu, 0x00000002u, 0x558d2adbu, 0x216d27a4u, 0xde92d860u, 0x00000002u, 0x00000002u, 0x216d27a2u, 0x216d27a2u, 0x216d27a0u, 0x00000000u, 0xde92d85fu, 0xde92d85du, 0xde92d85fu, 0x00000000u, 0x00001997u, 0x216d27a2u, 0xd5ed6edfu, 0x216d27a0u, 0xc5e84654u, 0x00000000u, 0x45880250u, 0xde92d85fu, 0x7f9fbbfbu, 0xfffffffdu, 0xeffad774u, 0xde92d85du, 0x2a129120u, 0xde92d85fu, 0x3a17b9abu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000008u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x0000000cu, 0x00000000u, 0x0000000cu, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x000002b9u, 0x216d27a0u, 0x216d27a6u, 0x216d27aau, 0x216d27b2u, 0x216d27c2u, 0x27a20002u, 0x216d0002u, 0x216d0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xa121d8c9u, 0xa161d909u, 0x69f0441eu, 0xa1a1d948u, 0xa121d8cau, 0xa161d909u, 0x0040003fu, 0x0040003fu, 0xa161d909u, 0xa121d936u, 0xa121d900u, 0xffffffc9u, 0x5e9e26c0u, 0x5ede26c9u, 0x00000000u, 0x0000093fu, 0xa161d93fu, 0xffffffffu, 0xa121d936u, 0x960fbbe1u, 0xa121d900u, 0x00000000u, 0xffffffc9u, 0x69f0441eu, 0xffbffff6u, 0x960fbbe1u, 0x5e9e26c0u, 0x00000000u, 0x5ede26c9u, 0x69f0441eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x5ede2736u, 0x4303b251u, 0x85c76463u, 0x0b4ec887u, 0x165d90cfu, 0x003fd909u, 0x0040d909u, 0x0040a161u, 0x0028589du, 0xffe8585eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x7aa2868bu, 0x00000021u, 0x00000040u, 0x7aa286cdu, 0x7aa2868bu, 0x00000021u, 0x00000021u, 0x7aa286acu, 0x7aa286acu, 0x7aa2868du, 0x7aa2868cu, 0xfffffffeu, 0x855d7952u, 0x855d7972u, 0x00000000u, 0x0000007fu, 0x7aa286adu, 0x40000040u, 0x7aa2868du, 0x40000040u, 0x7aa2868cu, 0x40000000u, 0xfffffffeu, 0xffffffbfu, 0xffffffdfu, 0xffffffffu, 0x855d7952u, 0xbfffffbfu, 0x855d7972u, 0xbfffffbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0xf5450d58u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x00001ffeu, 0x00000000u, 0xfffffffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x7aa2868bu, 0xf5450d79u, 0xea8a1ad1u, 0xd5143581u, 0xaa286ae1u, 0x002186acu, 0x000086acu, 0x00007aa2u, 0x0000000fu, 0x0000000fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002ffu}, - {0x7fc3987cu, 0x0010001cu, 0x10000000u, 0x7fe398b4u, 0x7fc3987cu, 0x0010001cu, 0x0010001cu, 0x7fd39898u, 0x7fd39898u, 0x7fc39884u, 0x7fc39880u, 0xfffffffbu, 0x802c6763u, 0x803c677bu, 0x00000000u, 0x0000007fu, 0x7fd3989cu, 0x3e1c5d3au, 0x7fc39884u, 0x3e1c5d3au, 0x7fc39880u, 0x2e1c5d3au, 0xfffffffbu, 0xefffffffu, 0xffefffe7u, 0xffffffffu, 0x802c6763u, 0xc1e3a2c5u, 0x803c677bu, 0xc1e3a2c5u, 0x00000000u, 0x0000007fu, 0x80000000u, 0xa7fd3989u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0xf0000000u, 0xf0000000u, 0x000fffffu, 0x00000007u, 0x00000007u, 0x0000d3a7u, 0x00000000u, 0x000003ffu, 0x7fc3987cu, 0xffb7314cu, 0xff5e627cu, 0xfeacc4dcu, 0xfd49899cu, 0x001c9898u, 0x00109898u, 0x00107fd3u, 0x0007fd47u, 0x0007fd47u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002ffu}, - {0x0020003au, 0x00000005u, 0x0010001cu, 0x00200044u, 0x0020003au, 0x00000005u, 0x00000005u, 0x0020003fu, 0x0020003fu, 0x0020003au, 0x0020003au, 0xffffffffu, 0xffdfffc0u, 0xffdfffc5u, 0x00000000u, 0x0000007fu, 0x0020003fu, 0xa9fd1cfdu, 0x0020003au, 0xa9ed1cf5u, 0x0020003au, 0xa9ed1ce1u, 0xffffffffu, 0xffffffebu, 0xfffffffau, 0xffeffff7u, 0xffdfffc0u, 0x5602e302u, 0xffdfffc5u, 0x5612e30au, 0x00000000u, 0x0000007fu, 0x040007e0u, 0x3fa39d20u, 0x00010001u, 0x48010001u, 0x054fe8e7u, 0x00010001u, 0x48010001u, 0xfd4fe8e7u, 0xffffffe0u, 0xffffffe0u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0020003au, 0x00400083u, 0x00800101u, 0x010001fdu, 0x020003f5u, 0x0005003fu, 0x0000003fu, 0x00000020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x4ed751d9u, 0x43cdc356u, 0xb0ddb48bu, 0x38c434d2u, 0x4ed751dau, 0xf4f6717cu, 0x43cdc356u, 0x43cdc356u, 0xf4f6717cu, 0xb73bb22au, 0x03098202u, 0x4bcdcfd7u, 0x08000c81u, 0x48c44dd5u, 0x00000000u, 0x000018dfu, 0xf7fff37eu, 0xb6dfbd9fu, 0xb73bb22au, 0x168f8d1eu, 0x03098202u, 0x108d840au, 0x4bcdcfd7u, 0xf9fdf6ebu, 0xbf3bbeabu, 0x5fafcf7eu, 0x08000c81u, 0x49204260u, 0x48c44dd5u, 0xe97072e1u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x60000000u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x00000004u, 0xfffffffbu, 0xffffffffu, 0xf0000000u, 0x00000000u, 0xf0000000u, 0x00000004u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x000003ffu, 0x4ed751dau, 0x7c91f828u, 0x042d7ed4u, 0x13648c2cu, 0x31d2a6dcu, 0x717cc356u, 0xf4f6c356u, 0xf4f643cdu, 0x40e163f6u, 0xfd13a0a0u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xffefffe4u, 0x0010001cu, 0x10000000u, 0x0010001cu, 0xffefffe4u, 0x00000000u, 0x00000000u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x00000000u, 0xffefffe3u, 0xffefffe3u, 0xffefffe3u, 0x00000000u, 0x00001197u, 0x0010001cu, 0x10080004u, 0x0010001cu, 0x10080004u, 0x00000000u, 0x00080004u, 0xffefffe3u, 0xefffffffu, 0xffffffffu, 0xffffffffu, 0xffefffe3u, 0xeff7fffbu, 0xffefffe3u, 0xeff7fffbu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x40000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000040u, 0x00000000u, 0x000002b9u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x001c0000u, 0x00100000u, 0x00100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x0f4ef612u, 0xbce240c6u, 0x68916d08u, 0x8913779eu, 0x0f4ef612u, 0xbce240c6u, 0xbce240c6u, 0xcc3136d8u, 0xcc3136d8u, 0x70d3761eu, 0x40113618u, 0xcf3dbff9u, 0x030c8921u, 0x8f2c89e1u, 0x00000000u, 0x0000007fu, 0xfcf376deu, 0x68916d1cu, 0x70d3761eu, 0x68816d14u, 0x40113618u, 0x00000014u, 0xcf3dbff9u, 0x977e92ffu, 0x73dfff3fu, 0xffeffff7u, 0x030c8921u, 0x976e92e3u, 0x8f2c89e1u, 0x977e92ebu, 0x00000000u, 0x0000007fu, 0x0c4db600u, 0x04000733u, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0xff30c4dbu, 0x7330c4dbu, 0x00004000u, 0x3fffffc0u, 0x3fffffc0u, 0x00000000u, 0xff30c4dbu, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0x000003ffu, 0x0f4ef612u, 0x5544ae76u, 0xeda71c26u, 0x1e6bf786u, 0x7ff5ae46u, 0x40c636d8u, 0xbce236d8u, 0xbce2cc31u, 0x96a89b63u, 0x0d9523c5u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xffff0020u, 0x00000040u, 0xb036d9ddu, 0x0001005fu, 0xffff0021u, 0x00000040u, 0x00000040u, 0x0001001fu, 0x0001001fu, 0x0001005fu, 0x00000040u, 0xfffeffe0u, 0xfffeffa0u, 0xfffeffa0u, 0x00000000u, 0x0000199fu, 0x0001005fu, 0xb4b6d9ffu, 0x0001005fu, 0x349210e2u, 0x00000040u, 0x301210c0u, 0xfffeffe0u, 0xfb7fffddu, 0xffffffffu, 0x7fdb36e2u, 0xfffeffa0u, 0x4b492600u, 0xfffeffa0u, 0xcb6def1du, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000020u, 0x00000000u, 0x606db3bau, 0x00000001u, 0x00000000u, 0x606db3bau, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x0000ffdfu, 0x0001009fu, 0x0001011fu, 0x0001021fu, 0x0001041fu, 0x001f0040u, 0x00010040u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x85d3ab9eu, 0x0000003fu, 0x0001001fu, 0x7a2c54dfu, 0x85d3ab9fu, 0x0000003fu, 0x0000003fu, 0x7a2c54a0u, 0x7a2c54a0u, 0x7a2c549fu, 0x0000001fu, 0x85d3ab7fu, 0x85d3ab40u, 0x85d3ab60u, 0x00000000u, 0x0000199fu, 0x7a2c54bfu, 0x7fffffffu, 0x7a2c549fu, 0x7ffeffe0u, 0x0000001fu, 0x00000000u, 0x85d3ab7fu, 0x8001001fu, 0xffffffdfu, 0xfffeffe0u, 0x85d3ab40u, 0x80000000u, 0x85d3ab60u, 0x8001001fu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000003fu, 0x0000003fu, 0x0001001fu, 0x00000000u, 0x0000003fu, 0x0001001fu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0xffffffffu, 0x0000003fu, 0x0000003fu, 0x0001001fu, 0x00000000u, 0x000003ffu, 0x7a2c5461u, 0x7a2c551eu, 0x7a2c559cu, 0x7a2c5698u, 0x7a2c5890u, 0x54a0003fu, 0x7a2c003fu, 0x7a2c0000u, 0x0000001eu, 0x0000001eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu} + {0xf7408821u, 0xbde5c099u, 0x00000001u, 0x848af910u, 0xf7408822u, 0xbde5c099u, 0xbde5c099u, 0xc6a53877u, 0xc6a53877u, 0x7b40f8eeu, 0x3940c088u, 0xbdffc799u, 0x001a0700u, 0x84bf0711u, 0x00000000u, 0x0000199fu, 0xffe5f8ffu, 0xa6233255u, 0x7b40f8eeu, 0xa6233254u, 0x3940c088u, 0x00000000u, 0xbdffc799u, 0x59dccdabu, 0x7b5affeeu, 0xfffffffeu, 0x001a0700u, 0x59dccdaau, 0x84bf0711u, 0x59dccdabu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4c800000u, 0x0000017bu, 0x00000000u, 0x00000000u, 0xffffff7bu, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0xff800000u, 0xffffff7bu, 0x0000017bu, 0x00000000u, 0x00000000u, 0x0000035eu, 0x08bf77deu, 0x4270b9a9u, 0xbe3c3adbu, 0xb5d33d3fu, 0xa5014207u, 0x3877c099u, 0xc6a5c099u, 0xc6a5bde5u, 0x935a41eeu, 0x0ecf48deu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x00000000u, 0x00000000u, 0xd86f17ecu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000007u, 0x00000000u, 0xde7f5fefu, 0x00000000u, 0x96355b43u, 0x00000000u, 0x06104803u, 0xffffffffu, 0x6fdaecbfu, 0xffffffffu, 0xb7b5fb53u, 0xffffffffu, 0x2180a010u, 0xffffffffu, 0x69caa4bcu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000003eu}, + {0xffbffffeu, 0x00400000u, 0x9abcdef0u, 0x003ffffeu, 0xffbffffeu, 0xfffffffeu, 0x00400000u, 0x00400000u, 0xfffffffeu, 0xffbffffeu, 0xffbffffeu, 0xffffffffu, 0x00000001u, 0x00400001u, 0x00000000u, 0x0000013fu, 0xfffffffeu, 0x9abcdef0u, 0xffbffffeu, 0x1abcdef0u, 0xffbffffeu, 0x00000000u, 0xffffffffu, 0xe543210fu, 0xffbfffffu, 0x7fffffffu, 0x00000001u, 0x6543210fu, 0x00400001u, 0xe543210fu, 0x00000000u, 0x0000007fu, 0xfffffffeu, 0x80000000u, 0xfffffffeu, 0xfffffffeu, 0x80000000u, 0xfffffffeu, 0xfffffffeu, 0x80000000u, 0x3fffffffu, 0xffffffffu, 0x3fffffffu, 0xfffffffeu, 0xfffffffeu, 0xfffffffeu, 0x80000000u, 0x000003ffu, 0x00400002u, 0x003ffffcu, 0x003ffff8u, 0x003ffff0u, 0x003fffe0u, 0x0000fffeu, 0x0040fffeu, 0x0040ffffu, 0x003fffffu, 0xffffffffu, 0xfffffffeu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x675a36e0u, 0x98a5c920u, 0x40000000u, 0x98a5c920u, 0x675a36e0u, 0x98a5c920u, 0x00000000u, 0x00000000u, 0x98a5c920u, 0x98a5c920u, 0x00000000u, 0x675a36dfu, 0x675a36dfu, 0x675a36dfu, 0x00000000u, 0x000010d7u, 0x98a5c920u, 0x40000000u, 0x98a5c920u, 0x00000000u, 0x00000000u, 0x00000000u, 0x675a36dfu, 0xffffffffu, 0xffffffffu, 0xbfffffffu, 0x675a36dfu, 0xbfffffffu, 0x675a36dfu, 0xffffffffu, 0x00000000u, 0x0000006fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x000000b9u, 0x675a36e0u, 0x98a5c920u, 0x98a5c920u, 0x98a5c920u, 0x98a5c920u, 0xc9200000u, 0x98a50000u, 0x98a50000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x7fffffffu, 0x7fffffffu, 0x00200000u, 0x7ffffffdu, 0x7fffffffu, 0xfffffffeu, 0x7fffffffu, 0x7fffffffu, 0xfffffffeu, 0x80000001u, 0x80000000u, 0xfffffffeu, 0x00000000u, 0x7ffffffeu, 0x00000000u, 0x0000033du, 0xffffffffu, 0xffffffffu, 0x80000001u, 0xffdfffffu, 0x80000000u, 0xffdfffffu, 0xfffffffeu, 0xffffffffu, 0x80000001u, 0xffdfffffu, 0x00000000u, 0x00000000u, 0x7ffffffeu, 0x00200000u, 0x00000000u, 0x0000007du, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000001u, 0x00000000u, 0x000001ffu, 0x7fffffffu, 0x7ffffffbu, 0x7ffffff7u, 0x7fffffefu, 0x7fffffdfu, 0xfffffffeu, 0x7ffffffeu, 0x7fffffffu, 0x7ffffffeu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xd75da9f0u, 0x741b324du, 0xb057c162u, 0xbf940e8au, 0xd75da9f0u, 0x4b78dc3du, 0x4b78dc3du, 0x741b324du, 0x741b324du, 0x3f63ee70u, 0x0b60cc30u, 0xcbfcddbfu, 0x80840182u, 0xc09c118fu, 0x00000000u, 0x0000159fu, 0x7f7bfe7du, 0xb057c17fu, 0x3f63ee70u, 0xb057c15du, 0x0b60cc30u, 0x0000001du, 0xcbfcddbfu, 0x4fa83ebfu, 0xbfe7eff2u, 0xffffffddu, 0x80840182u, 0x4fa83e80u, 0xc09c118fu, 0x4fa83ea2u, 0x00000000u, 0x0000007fu, 0x1b87a000u, 0x0007e96fu, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0xffffe000u, 0xffffe000u, 0xffffffffu, 0x00025bc6u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x000003ffu, 0x28a25610u, 0x0b0ceac7u, 0xa1fea341u, 0xcfe21435u, 0x2ba8f61du, 0x324ddc3du, 0x741bdc3du, 0x741b4b78u, 0x223ac85fu, 0x223ac85fu, 0x01fa5bc6u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xfffffffdu, 0x00000001u, 0x8a887204u, 0xffffffffu, 0xfffffffdu, 0xfffffffeu, 0x00000001u, 0x00000001u, 0xfffffffeu, 0xffffffffu, 0xfffffffeu, 0xfffffffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000013cu, 0xffffffffu, 0x8a887204u, 0xffffffffu, 0x0a887204u, 0xfffffffeu, 0x00000000u, 0xfffffffeu, 0xf5778dfbu, 0xffffffffu, 0x7fffffffu, 0x00000000u, 0x75778dfbu, 0x00000000u, 0xf5778dfbu, 0x00000000u, 0x0000007fu, 0xfffffffcu, 0x00000001u, 0x7fffffffu, 0x7fffffffu, 0x40000000u, 0xffffffffu, 0x7fffffffu, 0xc0000000u, 0x7ffffffeu, 0xfffffffeu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x00000003u, 0xfffffffdu, 0xfffffff9u, 0xfffffff1u, 0xffffffe1u, 0x0001fffeu, 0x0000fffeu, 0x0000ffffu, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x9ec733efu, 0x00008000u, 0x40000000u, 0x6139cc10u, 0x9ec733f0u, 0x00008000u, 0x00008000u, 0x61394c10u, 0x61394c10u, 0x6139cc10u, 0x00008000u, 0x9ec6b3efu, 0x9ec633efu, 0x9ec633efu, 0x00000000u, 0x0000199fu, 0x6139cc10u, 0x4000003fu, 0x6139cc10u, 0x4000003fu, 0x00008000u, 0x40000000u, 0x9ec6b3efu, 0xffffffc0u, 0xffffffffu, 0xffffffffu, 0x9ec633efu, 0xbfffffc0u, 0x9ec633efu, 0xbfffffc0u, 0x00000000u, 0x0000007fu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x000002b9u, 0x6138cc10u, 0x613a4c10u, 0x613b4c10u, 0x613d4c10u, 0x61414c10u, 0x4c108000u, 0x61398000u, 0x61390000u, 0x0000309cu, 0x0000309cu, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x7fffffffu, 0xffffffffu, 0x40000000u, 0x7ffffffeu, 0x80000000u, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x80000000u, 0x80000000u, 0xffffffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x0000093du, 0xffffffffu, 0x66868695u, 0x80000000u, 0x26868695u, 0x80000000u, 0x00000000u, 0xffffffffu, 0xd979796au, 0x80000000u, 0xbfffffffu, 0x00000000u, 0x9979796au, 0x7fffffffu, 0xd979796au, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x80000000u, 0x7ffffffdu, 0x7ffffffbu, 0x7ffffff7u, 0x7fffffefu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7ffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x9e86a671u, 0x617959ceu, 0xffff8000u, 0x61795a0du, 0x9e86a671u, 0x0000003fu, 0x0000003fu, 0x617959ceu, 0x617959ceu, 0x617959f1u, 0x00000031u, 0x9e86a63fu, 0x9e86a600u, 0x9e86a60eu, 0x00000000u, 0x0000119fu, 0x617959ffu, 0xffff8000u, 0x617959f1u, 0xbfff8000u, 0x00000031u, 0x00000000u, 0x9e86a63fu, 0x40007fffu, 0xfffffff1u, 0xbfffffffu, 0x9e86a600u, 0x00007fffu, 0x9e86a60eu, 0x40007fffu, 0x00000000u, 0x0000007fu, 0x000fc000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00010000u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x000002b9u, 0x6179598fu, 0x61795a4cu, 0x61795acau, 0x61795bc6u, 0x61795dbeu, 0x59ce003fu, 0x6179003fu, 0x61790000u, 0x00000017u, 0x00000017u, 0x00000000u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x3bfa6abbu, 0xc4069545u, 0x80000000u, 0xc4079545u, 0x3bfa6abbu, 0xc4069545u, 0x00010000u, 0x00010000u, 0xc4069545u, 0xc4079545u, 0x00010000u, 0x3bf96abau, 0x3bf86abau, 0x3bf86abau, 0x00000000u, 0x000010dfu, 0xc4079545u, 0x80000040u, 0xc4079545u, 0x80000040u, 0x00010000u, 0x00000040u, 0x3bf96abau, 0x7fffffffu, 0xffffffffu, 0xffffffffu, 0x3bf86abau, 0x7fffffbfu, 0x3bf86abau, 0x7fffffbfu, 0x00000000u, 0x0000007fu, 0x00200000u, 0x00000800u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x3bfa6abbu, 0xc4089545u, 0xc40a9545u, 0xc40e9545u, 0xc4169545u, 0x95450000u, 0xc4060000u, 0xc4060001u, 0x0000c406u, 0xffffc406u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0x10d822c6u, 0x015c33b2u, 0x00010000u, 0x13908a2au, 0x10d822c6u, 0x015c33b2u, 0x015c33b2u, 0x12345678u, 0x12345678u, 0x136865cau, 0x12204448u, 0xfeb7de7du, 0xec838805u, 0xec979a35u, 0x00000000u, 0x0000007fu, 0x137c77fau, 0x00010000u, 0x136865cau, 0x00010000u, 0x12204448u, 0x00000000u, 0xfeb7de7du, 0xfffeffffu, 0xffebedcfu, 0xffffffffu, 0xec838805u, 0xfffeffffu, 0xec979a35u, 0xfffeffffu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x59e00000u, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000048du, 0x00000000u, 0x00000000u, 0xfffc0000u, 0x00000000u, 0xfffc0000u, 0x0000048du, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000035eu, 0x10d822c6u, 0x25c4e0a2u, 0x4a2d8d92u, 0x92fee772u, 0x24a19b32u, 0x33b25678u, 0x015c5678u, 0x015c1234u, 0x0018c2d2u, 0x0018c2d2u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000047eu}, + {0x2a74915au, 0x0000ffffu, 0x00010000u, 0x2a769158u, 0x2a74915au, 0x0000ffffu, 0x0000ffffu, 0x2a759159u, 0x2a759159u, 0x2a756ea6u, 0x2a750000u, 0xffff9159u, 0xd58a0000u, 0xd58a9159u, 0x00000000u, 0x0000007fu, 0x2a75ffffu, 0x4a0fe75du, 0x2a756ea6u, 0x4a0ee75du, 0x2a750000u, 0x4a0ee75du, 0xffff9159u, 0xffffffffu, 0xffff6ea6u, 0xfffeffffu, 0xd58a0000u, 0xb5f018a2u, 0xd58a9159u, 0xb5f118a2u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x2a74915au, 0x54ec22b1u, 0xa9d74563u, 0x53ad8ac7u, 0xa75a158fu, 0xffff9159u, 0x00009159u, 0x00002a75u, 0x00002a75u, 0x00002a75u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000004feu}, + {0x0000fffdu, 0x0000ffffu, 0xe4cc4132u, 0x00010000u, 0x0000fffeu, 0x00000001u, 0x00000001u, 0x0000ffffu, 0x0000ffffu, 0x0000fffeu, 0x0000fffeu, 0xffffffffu, 0xffff0000u, 0xffff0001u, 0x00000000u, 0x0000087fu, 0x0000ffffu, 0xe4cc413fu, 0x0000fffeu, 0xe4cc410du, 0x0000fffeu, 0xe4cc4100u, 0xffffffffu, 0xfffffff2u, 0xfffffffeu, 0xffffffcdu, 0xffff0000u, 0x1b33bec0u, 0xffff0001u, 0x1b33bef2u, 0x00000000u, 0x0000007fu, 0x0001fffeu, 0xc9988264u, 0x00007fffu, 0x00007fffu, 0x72662099u, 0x00007fffu, 0x00007fffu, 0xf2662099u, 0xfffffffeu, 0xfffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0000fffeu, 0x0001ffffu, 0x0003fffdu, 0x0007fff9u, 0x000ffff1u, 0x0001ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0xad7bd36cu, 0x43892dfcu, 0x00000005u, 0xd996888bu, 0xad7bd36du, 0x960d5a8fu, 0x43892dfcu, 0x43892dfcu, 0x960d5a8fu, 0xd5847773u, 0x41802570u, 0x6bfbadfcu, 0x28728000u, 0x2a7b888cu, 0x00000000u, 0x00001adfu, 0xd78d7fffu, 0x0021001fu, 0xd5847773u, 0x0021001au, 0x41802570u, 0x00000000u, 0x6bfbadfcu, 0xffdeffe5u, 0xfdf6f773u, 0xfffffffau, 0x28728000u, 0xffdeffe0u, 0x2a7b888cu, 0xffdeffe5u, 0x00000000u, 0x0000007fu, 0x96fe0000u, 0x0002a1c4u, 0x00008712u, 0x000a8712u, 0x00000000u, 0x00008712u, 0x000a8712u, 0x00000000u, 0xffff8000u, 0xffff8000u, 0xffffffffu, 0x00000712u, 0x00000712u, 0x00000712u, 0x00000000u, 0x000003ffu, 0x52842c93u, 0x1d1fb687u, 0xa432127fu, 0xb256ca6fu, 0xcea03a4fu, 0x5a8f2dfcu, 0x960d2dfcu, 0x960d4389u, 0x2795e6ccu, 0xe40cb8d0u, 0x00000712u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xd0d2b3b9u, 0x2f4d4c86u, 0x00000040u, 0x2f6d4cc5u, 0xd0d2b3b9u, 0x0020003fu, 0x0020003fu, 0x2f4d4c86u, 0x2f4d4c86u, 0x2f6d4cb9u, 0x00200039u, 0xd0b2b37fu, 0xd092b340u, 0xd092b346u, 0x00000000u, 0x0000119fu, 0x2f6d4cbfu, 0x7b121dc5u, 0x2f6d4cb9u, 0x7b121d85u, 0x00200039u, 0x7b121d85u, 0xd0b2b37fu, 0xffffffffu, 0xfffffff9u, 0xffffffbfu, 0xd092b340u, 0x84ede23au, 0xd092b346u, 0x84ede27au, 0x00000000u, 0x0000007fu, 0x08000fc0u, 0xc4877140u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0xffffffc0u, 0xffffffc0u, 0xffffffffu, 0x00008000u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x000003ffu, 0x2f2d4c47u, 0x2f8d4d04u, 0x2fcd4d82u, 0x304d4e7eu, 0x314d5076u, 0x4c86003fu, 0x2f4d003fu, 0x2f4d0020u, 0x0005e9b5u, 0x0005e9b5u, 0x14008000u, 0x01ec4877u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xfb5fed18u, 0x00000001u, 0x5b8e8fb2u, 0x04a012e9u, 0xfb5fed19u, 0x00000001u, 0x00000001u, 0x04a012e8u, 0x04a012e8u, 0x04a012e9u, 0x00000001u, 0xfb5fed17u, 0xfb5fed16u, 0xfb5fed16u, 0x00000000u, 0x0000199fu, 0x04a012e9u, 0x5bcf8fb3u, 0x04a012e9u, 0x5bcf8fb1u, 0x00000001u, 0x5b8e8fb0u, 0xfb5fed17u, 0xffbefffeu, 0xffffffffu, 0xfffffffdu, 0xfb5fed16u, 0xa430704cu, 0xfb5fed16u, 0xa430704eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x000002b9u, 0x04a012e7u, 0x04a012eau, 0x04a012ecu, 0x04a012f0u, 0x04a012f8u, 0x12e80001u, 0x04a00001u, 0x04a00000u, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x0baccdb2u, 0xf453324eu, 0xffff8000u, 0xf453324eu, 0x0baccdb2u, 0xf453324eu, 0x00000000u, 0x00000000u, 0xf453324eu, 0xf453324eu, 0x00000000u, 0x0baccdb1u, 0x0baccdb1u, 0x0baccdb1u, 0x00000000u, 0x000010d7u, 0xf453324eu, 0xffff8040u, 0xf453324eu, 0xffff8040u, 0x00000000u, 0x00000040u, 0x0baccdb1u, 0x00007fffu, 0xffffffffu, 0xffffffffu, 0x0baccdb1u, 0x00007fbfu, 0x0baccdb1u, 0x00007fbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00100000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x000002b9u, 0x0baccdb2u, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0x324e0000u, 0xf4530000u, 0xf4530000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xf121c8c2u, 0x00080004u, 0x1ba95a54u, 0xf131c8cau, 0xf121c8c2u, 0xf129c8c6u, 0x00080004u, 0x00080004u, 0xf129c8c6u, 0xf121c8c2u, 0xf121c8c2u, 0xffffffffu, 0x0ed63739u, 0x0ede373du, 0x00000000u, 0x0000013fu, 0xf129c8c6u, 0x1ba95a54u, 0xf121c8c2u, 0x0ba95a54u, 0xf121c8c2u, 0x00000000u, 0xffffffffu, 0xf456a5abu, 0xfff7fffbu, 0xefffffffu, 0x0ed63739u, 0xe456a5abu, 0x0ede373du, 0xf456a5abu, 0x00000000u, 0x0000007fu, 0x129c8c60u, 0x0000000fu, 0x0f129c8cu, 0x0f129c8cu, 0x01000000u, 0xff129c8cu, 0x0f129c8cu, 0x01000000u, 0x000003f0u, 0x000003f0u, 0x00000000u, 0xffffff8cu, 0x0000008cu, 0x0000008cu, 0x00000000u, 0x000003ffu, 0x0ede373eu, 0xe25b9190u, 0xc4af231cu, 0x89564634u, 0x12a48c64u, 0x0004c8c6u, 0x0008c8c6u, 0x0008f129u, 0x00078952u, 0xffff894eu, 0xffffff8cu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x80010020u, 0x7fffffffu, 0x10000000u, 0x8001001eu, 0x80010020u, 0x0001001fu, 0x0001001fu, 0x7fffffffu, 0x7fffffffu, 0x7ffeffe0u, 0x00000000u, 0x8001001fu, 0x80000000u, 0x8001001fu, 0x00000000u, 0x00001597u, 0x7fffffffu, 0x1960afe9u, 0x7ffeffe0u, 0x1960afe9u, 0x00000000u, 0x0960afe9u, 0x8001001fu, 0xefffffffu, 0xfffeffe0u, 0xffffffffu, 0x80000000u, 0xe69f5016u, 0x8001001fu, 0xe69f5016u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x7ffeffe0u, 0x8002003du, 0x8004007bu, 0x800800f7u, 0x801001efu, 0xffff001fu, 0x7fff001fu, 0x7fff0001u, 0x0000800fu, 0x0000800fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0x5f7933bfu, 0x8b8e8f4eu, 0x00400000u, 0x7696525bu, 0x5f7933bfu, 0x8b8e8f4eu, 0x8b8e8f4eu, 0xeb07c30du, 0xeb07c30du, 0x60894c43u, 0x60014001u, 0xff77f3bdu, 0x147030b0u, 0x9f76b3bcu, 0x00000000u, 0x0000047fu, 0xeb8fcf4fu, 0x0050001cu, 0x60894c43u, 0x0050001cu, 0x60014001u, 0x0010001cu, 0xff77f3bdu, 0xffbfffffu, 0x74f97cf3u, 0xffffffffu, 0x147030b0u, 0xffafffe3u, 0x9f76b3bcu, 0xffafffe3u, 0x00000000u, 0x0000007fu, 0xf0c34000u, 0x00073ac1u, 0x0003ac1fu, 0x0073ac1fu, 0x00000040u, 0xffffac1fu, 0x0073ac1fu, 0x00000040u, 0x07ffc000u, 0x07ffc000u, 0x00000000u, 0xffffec1fu, 0x00002c1fu, 0x00002c1fu, 0x00000000u, 0x000003ffu, 0x5f7933bfu, 0x619e1568u, 0x37ad9b82u, 0xe3cca7b6u, 0x3c0ac01eu, 0x8f4ec30du, 0x8b8ec30du, 0x8b8eeb07u, 0x802018c7u, 0x0989c66cu, 0xffffec1fu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x80d682f0u, 0x7fffffffu, 0xb89c4e56u, 0x7f297d0du, 0x80d682f1u, 0xff297d0eu, 0x7fffffffu, 0x7fffffffu, 0xff297d0eu, 0x80d682f1u, 0x00d682f1u, 0x7fffffffu, 0x00000000u, 0x7f297d0eu, 0x00000000u, 0x00001addu, 0xffffffffu, 0xffffffffu, 0x80d682f1u, 0x4763b1a9u, 0x00d682f1u, 0x00000000u, 0x7fffffffu, 0xb89c4e56u, 0x80d682f1u, 0x4763b1a9u, 0x00000000u, 0x00000000u, 0x7f297d0eu, 0xb89c4e56u, 0x00000000u, 0x0000007du, 0xffffc000u, 0x13959fffu, 0x0001ffffu, 0x3959ffffu, 0x0002e271u, 0x0001ffffu, 0x3959ffffu, 0xfffee271u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x0001ffffu, 0x0001ffffu, 0x3959ffffu, 0x00000071u, 0x000003ffu, 0x7f297d0fu, 0xff297d0cu, 0xff297d0au, 0xff297d06u, 0xff297cfeu, 0x7d0effffu, 0xff29ffffu, 0xff297fffu, 0x7f94be86u, 0xff94be87u, 0x3959ffffu, 0x00000071u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x8bd8883au, 0x74a677c6u, 0x2547f19cu, 0x752577c6u, 0x8bd8883au, 0x007f0000u, 0x007f0000u, 0x74a677c6u, 0x74a677c6u, 0x74d977c6u, 0x00590000u, 0x8b7f8839u, 0x8b008839u, 0x8b268839u, 0x00000000u, 0x0000119fu, 0x74ff77c6u, 0x2567f1bfu, 0x74d977c6u, 0x2567f1a3u, 0x00590000u, 0x00200023u, 0x8b7f8839u, 0xdab80e7fu, 0xffd9ffffu, 0xffffffe3u, 0x8b008839u, 0xda980e40u, 0x8b268839u, 0xda980e5cu, 0x00000000u, 0x0000007fu, 0x1fc00000u, 0x08000fc0u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0001fc00u, 0x0001fc00u, 0xfc01fc00u, 0x00000000u, 0x000003ffu, 0x742777c6u, 0x75a477c6u, 0x76a277c6u, 0x789e77c6u, 0x7c9677c6u, 0x77c60000u, 0x74a60000u, 0x74a6007fu, 0x0039de95u, 0x0039de95u, 0xfc01fc00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xde92d85fu, 0x00000002u, 0x558d2adbu, 0x216d27a4u, 0xde92d860u, 0x00000002u, 0x00000002u, 0x216d27a2u, 0x216d27a2u, 0x216d27a0u, 0x00000000u, 0xde92d85fu, 0xde92d85du, 0xde92d85fu, 0x00000000u, 0x00001997u, 0x216d27a2u, 0xd5ed6edfu, 0x216d27a0u, 0xc5e84654u, 0x00000000u, 0x45880250u, 0xde92d85fu, 0x7f9fbbfbu, 0xfffffffdu, 0xeffad774u, 0xde92d85du, 0x2a129120u, 0xde92d85fu, 0x3a17b9abu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000008u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x0000000cu, 0x00000000u, 0x0000000cu, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x000002b9u, 0x216d27a0u, 0x216d27a6u, 0x216d27aau, 0x216d27b2u, 0x216d27c2u, 0x27a20002u, 0x216d0002u, 0x216d0000u, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xa121d8c9u, 0xa161d909u, 0x69f0441eu, 0xa1a1d948u, 0xa121d8cau, 0xa161d909u, 0x0040003fu, 0x0040003fu, 0xa161d909u, 0xa121d936u, 0xa121d900u, 0xffffffc9u, 0x5e9e26c0u, 0x5ede26c9u, 0x00000000u, 0x0000093fu, 0xa161d93fu, 0xffffffffu, 0xa121d936u, 0x960fbbe1u, 0xa121d900u, 0x00000000u, 0xffffffc9u, 0x69f0441eu, 0xffbffff6u, 0x960fbbe1u, 0x5e9e26c0u, 0x00000000u, 0x5ede26c9u, 0x69f0441eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x5ede2736u, 0x4303b251u, 0x85c76463u, 0x0b4ec887u, 0x165d90cfu, 0x003fd909u, 0x0040d909u, 0x0040a161u, 0x0028589du, 0xffe8585eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x7aa2868bu, 0x00000021u, 0x00000040u, 0x7aa286cdu, 0x7aa2868bu, 0x00000021u, 0x00000021u, 0x7aa286acu, 0x7aa286acu, 0x7aa2868du, 0x7aa2868cu, 0xfffffffeu, 0x855d7952u, 0x855d7972u, 0x00000000u, 0x0000007fu, 0x7aa286adu, 0x40000040u, 0x7aa2868du, 0x40000040u, 0x7aa2868cu, 0x40000000u, 0xfffffffeu, 0xffffffbfu, 0xffffffdfu, 0xffffffffu, 0x855d7952u, 0xbfffffbfu, 0x855d7972u, 0xbfffffbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0xf5450d58u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x00001ffeu, 0x00000000u, 0xfffffffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x7aa2868bu, 0xf5450d79u, 0xea8a1ad1u, 0xd5143581u, 0xaa286ae1u, 0x002186acu, 0x000086acu, 0x00007aa2u, 0x0000000fu, 0x0000000fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000005feu}, + {0x7fc3987cu, 0x0010001cu, 0x10000000u, 0x7fe398b4u, 0x7fc3987cu, 0x0010001cu, 0x0010001cu, 0x7fd39898u, 0x7fd39898u, 0x7fc39884u, 0x7fc39880u, 0xfffffffbu, 0x802c6763u, 0x803c677bu, 0x00000000u, 0x0000007fu, 0x7fd3989cu, 0x3e1c5d3au, 0x7fc39884u, 0x3e1c5d3au, 0x7fc39880u, 0x2e1c5d3au, 0xfffffffbu, 0xefffffffu, 0xffefffe7u, 0xffffffffu, 0x802c6763u, 0xc1e3a2c5u, 0x803c677bu, 0xc1e3a2c5u, 0x00000000u, 0x0000007fu, 0x80000000u, 0xa7fd3989u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0xf0000000u, 0xf0000000u, 0x000fffffu, 0x00000007u, 0x00000007u, 0x0000d3a7u, 0x00000000u, 0x000003ffu, 0x7fc3987cu, 0xffb7314cu, 0xff5e627cu, 0xfeacc4dcu, 0xfd49899cu, 0x001c9898u, 0x00109898u, 0x00107fd3u, 0x0007fd47u, 0x0007fd47u, 0xffffd3a7u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000005ffu}, + {0x0020003au, 0x00000005u, 0x0010001cu, 0x00200044u, 0x0020003au, 0x00000005u, 0x00000005u, 0x0020003fu, 0x0020003fu, 0x0020003au, 0x0020003au, 0xffffffffu, 0xffdfffc0u, 0xffdfffc5u, 0x00000000u, 0x0000007fu, 0x0020003fu, 0xa9fd1cfdu, 0x0020003au, 0xa9ed1cf5u, 0x0020003au, 0xa9ed1ce1u, 0xffffffffu, 0xffffffebu, 0xfffffffau, 0xffeffff7u, 0xffdfffc0u, 0x5602e302u, 0xffdfffc5u, 0x5612e30au, 0x00000000u, 0x0000007fu, 0x040007e0u, 0x3fa39d20u, 0x00010001u, 0x48010001u, 0x054fe8e7u, 0x00010001u, 0x48010001u, 0xfd4fe8e7u, 0xffffffe0u, 0xffffffe0u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0020003au, 0x00400083u, 0x00800101u, 0x010001fdu, 0x020003f5u, 0x0005003fu, 0x0000003fu, 0x00000020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0x4ed751d9u, 0x43cdc356u, 0xb0ddb48bu, 0x38c434d2u, 0x4ed751dau, 0xf4f6717cu, 0x43cdc356u, 0x43cdc356u, 0xf4f6717cu, 0xb73bb22au, 0x03098202u, 0x4bcdcfd7u, 0x08000c81u, 0x48c44dd5u, 0x00000000u, 0x000018dfu, 0xf7fff37eu, 0xb6dfbd9fu, 0xb73bb22au, 0x168f8d1eu, 0x03098202u, 0x108d840au, 0x4bcdcfd7u, 0xf9fdf6ebu, 0xbf3bbeabu, 0x5fafcf7eu, 0x08000c81u, 0x49204260u, 0x48c44dd5u, 0xe97072e1u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x60000000u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x00000004u, 0xfffffffbu, 0xffffffffu, 0xf0000000u, 0x00000000u, 0xf0000000u, 0x00000004u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x000003ffu, 0x4ed751dau, 0x7c91f828u, 0x042d7ed4u, 0x13648c2cu, 0x31d2a6dcu, 0x717cc356u, 0xf4f6c356u, 0xf4f643cdu, 0x40e163f6u, 0xfd13a0a0u, 0xfffffffbu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xffefffe4u, 0x0010001cu, 0x10000000u, 0x0010001cu, 0xffefffe4u, 0x00000000u, 0x00000000u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x00000000u, 0xffefffe3u, 0xffefffe3u, 0xffefffe3u, 0x00000000u, 0x00001197u, 0x0010001cu, 0x10080004u, 0x0010001cu, 0x10080004u, 0x00000000u, 0x00080004u, 0xffefffe3u, 0xefffffffu, 0xffffffffu, 0xffffffffu, 0xffefffe3u, 0xeff7fffbu, 0xffefffe3u, 0xeff7fffbu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x40000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000040u, 0x00000000u, 0x000002b9u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x001c0000u, 0x00100000u, 0x00100000u, 0x00000000u, 0x00000000u, 0x00000040u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x0f4ef612u, 0xbce240c6u, 0x68916d08u, 0x8913779eu, 0x0f4ef612u, 0xbce240c6u, 0xbce240c6u, 0xcc3136d8u, 0xcc3136d8u, 0x70d3761eu, 0x40113618u, 0xcf3dbff9u, 0x030c8921u, 0x8f2c89e1u, 0x00000000u, 0x0000007fu, 0xfcf376deu, 0x68916d1cu, 0x70d3761eu, 0x68816d14u, 0x40113618u, 0x00000014u, 0xcf3dbff9u, 0x977e92ffu, 0x73dfff3fu, 0xffeffff7u, 0x030c8921u, 0x976e92e3u, 0x8f2c89e1u, 0x977e92ebu, 0x00000000u, 0x0000007fu, 0x0c4db600u, 0x04000733u, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0xff30c4dbu, 0x7330c4dbu, 0x00004000u, 0x3fffffc0u, 0x3fffffc0u, 0x00000000u, 0xff30c4dbu, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0x000003ffu, 0x0f4ef612u, 0x5544ae76u, 0xeda71c26u, 0x1e6bf786u, 0x7ff5ae46u, 0x40c636d8u, 0xbce236d8u, 0xbce2cc31u, 0x96a89b63u, 0x0d9523c5u, 0x7330c4dbu, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xffff0020u, 0x00000040u, 0xb036d9ddu, 0x0001005fu, 0xffff0021u, 0x00000040u, 0x00000040u, 0x0001001fu, 0x0001001fu, 0x0001005fu, 0x00000040u, 0xfffeffe0u, 0xfffeffa0u, 0xfffeffa0u, 0x00000000u, 0x0000199fu, 0x0001005fu, 0xb4b6d9ffu, 0x0001005fu, 0x349210e2u, 0x00000040u, 0x301210c0u, 0xfffeffe0u, 0xfb7fffddu, 0xffffffffu, 0x7fdb36e2u, 0xfffeffa0u, 0x4b492600u, 0xfffeffa0u, 0xcb6def1du, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000020u, 0x00000000u, 0x606db3bau, 0x00000001u, 0x00000000u, 0x606db3bau, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x0000ffdfu, 0x0001009fu, 0x0001011fu, 0x0001021fu, 0x0001041fu, 0x001f0040u, 0x00010040u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu} }; -constexpr const char* Names[64] = {"subb_u32", "cselect_b64 lo", "cselect_b64 hi", "add_i32", "sub_i32", "min_i32", "min_u32", "max_i32", "max_u32", "xor_b32", "andn2_b32", "orn2_b32", "nor_b32", "xnor_b32", nullptr, "scc subb_u32,cselect_b64,add_i32,sub_i32,min_i32,min_u32,max_i32,max_u32,xor_b32,andn2_b32,orn2_b32,nor_b32,xnor_b32", "or_b64 lo", "or_b64 hi", "xor_b64 lo", "xor_b64 hi", "andn2_b64 lo", "andn2_b64 hi", "orn2_b64 lo", "orn2_b64 hi", "nand_b64 lo", "nand_b64 hi", "nor_b64 lo", "nor_b64 hi", "xnor_b64 lo", "xnor_b64 hi", nullptr, "scc or_b64,xor_b64,andn2_b64,orn2_b64,nand_b64,nor_b64,xnor_b64", "lshl_b64 lo", "lshl_b64 hi", "lshr_b32", "lshr_b64 lo", "lshr_b64 hi", "ashr_i32", "ashr_i64 lo", "ashr_i64 hi", "bfm_b32", "bfm_b64 lo", "bfm_b64 hi", "bfe_i32", "bfe_u32", "bfe_u64 lo", "bfe_u64 hi", "scc lshl_b64,lshr_b32,lshr_b64,ashr_i32,ashr_i64,bfm_b32,bfm_b64,bfe_i32,bfe_u32,bfe_u64", "absdiff_i32", "lshl1_add_u32", "lshl2_add_u32", "lshl3_add_u32", "lshl4_add_u32", "pack_ll_b32_b16", "pack_lh_b32_b16", "pack_hh_b32_b16", "mul_hi_u32", "mul_hi_i32", nullptr, nullptr, nullptr, nullptr, nullptr, "scc absdiff_i32,lshl1_add_u32,lshl2_add_u32,lshl3_add_u32,lshl4_add_u32,pack_ll_b32_b16,pack_lh_b32_b16,pack_hh_b32_b16,mul_hi_u32,mul_hi_i32"}; +constexpr const char* Names[64] = {"subb_u32", "cselect_b64 lo", "cselect_b64 hi", "add_i32", "sub_i32", "min_i32", "min_u32", "max_i32", "max_u32", "xor_b32", "andn2_b32", "orn2_b32", "nor_b32", "xnor_b32", nullptr, "scc subb_u32,cselect_b64,add_i32,sub_i32,min_i32,min_u32,max_i32,max_u32,xor_b32,andn2_b32,orn2_b32,nor_b32,xnor_b32", "or_b64 lo", "or_b64 hi", "xor_b64 lo", "xor_b64 hi", "andn2_b64 lo", "andn2_b64 hi", "orn2_b64 lo", "orn2_b64 hi", "nand_b64 lo", "nand_b64 hi", "nor_b64 lo", "nor_b64 hi", "xnor_b64 lo", "xnor_b64 hi", nullptr, "scc or_b64,xor_b64,andn2_b64,orn2_b64,nand_b64,nor_b64,xnor_b64", "lshl_b64 lo", "lshl_b64 hi", "lshr_b32", "lshr_b64 lo", "lshr_b64 hi", "ashr_i32", "ashr_i64 lo", "ashr_i64 hi", "bfm_b32", "bfm_b64 lo", "bfm_b64 hi", "bfe_i32", "bfe_u32", "bfe_u64 lo", "bfe_u64 hi", "scc lshl_b64,lshr_b32,lshr_b64,ashr_i32,ashr_i64,bfm_b32,bfm_b64,bfe_i32,bfe_u32,bfe_u64", "absdiff_i32", "lshl1_add_u32", "lshl2_add_u32", "lshl3_add_u32", "lshl4_add_u32", "pack_ll_b32_b16", "pack_lh_b32_b16", "pack_hh_b32_b16", "mul_hi_u32", "mul_hi_i32", "bfe_i64 lo", "bfe_i64 hi", nullptr, nullptr, nullptr, "scc absdiff_i32,lshl1_add_u32,lshl2_add_u32,lshl3_add_u32,lshl4_add_u32,pack_ll_b32_b16,pack_lh_b32_b16,pack_hh_b32_b16,mul_hi_u32,mul_hi_i32,bfe_i64"}; void Fill(std::uint32_t tid, std::uint32_t* words) { std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index ef4daa6c7..282aec345 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -270,6 +270,7 @@ enum class RdnaOpcode : std::uint16_t { SMulHiI32, SMulkI32, SBfeU64, + SBfeI64, SBfmB32, SBfmB64, SCselectB32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp index 0171c61c9..187c31d13 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp @@ -131,6 +131,7 @@ bool IsScalarAluOpcode(RdnaOpcode opcode) { case RdnaOpcode::SMulHiI32: case RdnaOpcode::SMulkI32: case RdnaOpcode::SBfeU64: + case RdnaOpcode::SBfeI64: case RdnaOpcode::SBfmB32: case RdnaOpcode::SBfmB64: case RdnaOpcode::SCselectB32: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index de3c85f62..c88eb7de7 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -118,6 +118,7 @@ RdnaOpcode decodeSop2Opcode(std::uint32_t opcode) { case 0x27u: return RdnaOpcode::SBfeU32; case 0x28u: return RdnaOpcode::SBfeI32; case 0x29u: return RdnaOpcode::SBfeU64; + case 0x2au: return RdnaOpcode::SBfeI64; case 0x2cu: return RdnaOpcode::SAbsdiffI32; case 0x2eu: return RdnaOpcode::SLshl1AddU32; case 0x2fu: return RdnaOpcode::SLshl2AddU32; @@ -279,6 +280,7 @@ std::uint32_t scalarDestinationDwordCount(RdnaOpcode opcode) { case RdnaOpcode::SNandB64: case RdnaOpcode::SNorB64: case RdnaOpcode::SXnorB64: + case RdnaOpcode::SBfeI64: case RdnaOpcode::SLshlB64: case RdnaOpcode::SLshrB64: case RdnaOpcode::SBfmB64: diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 243a1ed5b..298312dbf 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -239,7 +239,7 @@ private: IrU32 extractBits32(IrU32 source, IrU32 offset, IrU32 rawCount, bool sign); bool sBfmB64(const RdnaInstruction& inst); bool sBfeU32(const RdnaInstruction& inst, bool sign); - bool sBfeU64(const RdnaInstruction& inst); + bool sBfeU64(const RdnaInstruction& inst, bool sign); bool vBfeU32(const RdnaInstruction& inst, bool sign); bool vBfiB32(const RdnaInstruction& inst); bool sBitcmpB32(const RdnaInstruction& inst, bool expected); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index ba6b25f67..af1b30ba1 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -672,7 +672,7 @@ bool TranslationContext::sBfeU32(const RdnaInstruction& inst, bool sign) { return true; } -bool TranslationContext::sBfeU64(const RdnaInstruction& inst) { +bool TranslationContext::sBfeU64(const RdnaInstruction& inst, bool sign) { const IrU64 source = readU64(sourceAt(inst, 0u)); const IrU32 field = readU32(sourceAt(inst, 1u)); const IrU32 offset(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&field.Value(), &ir.Constant(0u), &ir.Constant(6u)})); @@ -680,8 +680,16 @@ bool TranslationContext::sBfeU64(const RdnaInstruction& inst) { const IrU32 available(ir.ISub(ir.Constant(64u), offset.Value())); const IrU32 count(ir.Emit(IrOpcode::UMin32, IrType::U32, {&rawCount.Value(), &available.Value()})); const IrU64 shifted(ir.Emit(IrOpcode::ShiftRightLogical64, IrType::U64, {&source.Value(), &offset.Value()})); - const IrU64 mask = rightMask64(count); - const IrU64 result(ir.Emit(IrOpcode::BitwiseAnd64, IrType::U64, {&shifted.Value(), &mask.Value()})); + IrU64 mask = rightMask64(count); + IrU64 extended = shifted; + if (sign) { + const IrU32 discard(ir.BitwiseAnd(ir.ISub(ir.Constant(64u), count.Value()), ir.Constant(63u))); + const IrU64 aligned(ir.Emit(IrOpcode::ShiftLeftLogical64, IrType::U64, {&shifted.Value(), &discard.Value()})); + extended = IrU64(ir.Emit(IrOpcode::ShiftRightArithmetic64, IrType::U64, {&aligned.Value(), &discard.Value()})); + const IrU32 keep(ir.Select(ir.INotEqual(count.Value(), ir.Constant(0u)), ir.Constant(0xffffffffu), ir.Constant(0u))); + mask = IrU64(ir.ConstructU64(keep.Value(), keep.Value())); + } + const IrU64 result(ir.Emit(IrOpcode::BitwiseAnd64, IrType::U64, {&extended.Value(), &mask.Value()})); writeOperand(inst.destination, &result.Value()); ir.SetScc(ir.Emit(IrOpcode::INotEqual64, IrType::U1, {&result.Value(), &ir.ConstantU64(0)})); return true; diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index 1a3051635..505d91545 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -381,7 +381,9 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SBfeI32: return sBfeU32(inst, true); case RdnaOpcode::SBfeU64: - return sBfeU64(inst); + return sBfeU64(inst, false); + case RdnaOpcode::SBfeI64: + return sBfeU64(inst, true); case RdnaOpcode::SBitcmp0B32: return sBitcmpB32(inst, false); case RdnaOpcode::SBitcmp1B32: From be6c0cf3ec33dea639b875b6326c36aaceaa51a1 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Thu, 1 Oct 2026 18:30:29 +0200 Subject: [PATCH 097/203] fix(json2): reject numbers that overflow to infinity The parser stored strtod's result for 1e309 and similar numbers, so a document with such a value parsed as an infinite real. Serializing it then threw "cannot serialize a non-finite number", which escaped the guest call and terminated the process. sce::Json::Parser::parse now returns JsonErrorParse for a number that is not finite after conversion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- core/libs/prx/libSceJson2/Export.cpp | 4 +++- core/libs/tests/GuestJson.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index a19fc3289..789de13a8 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -371,8 +371,10 @@ private: } } } + const double real = std::strtod(text.c_str(), nullptr); + if (!std::isfinite(real)) return false; out.type = TypeReal; - out.real = std::strtod(text.c_str(), nullptr); + out.real = real; return true; } bool value(Node& out) { diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index e30b0b123..4f3f3d01b 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -163,7 +163,7 @@ static void ParseAndRoundTrip() { Require(Serialize(reparsed) == serialized); _ZN3sce4Json5ValueD1Ev(&reparsed); - for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru", "{} x", "01"}) { + for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru", "{} x", "01", "1e309", "[-1e309]", "{\"a\":1e400}"}) { Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } From bdeeb17326e19684b50774ad17148daab68ed1fd Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Mon, 5 Oct 2026 08:43:53 +0200 Subject: [PATCH 098/203] fix(json2): throw on numbers that overflow a double Whether the console's parser rejects 1e309 or stores an infinite real is unknown, and RFC 8259 allows both. Following the exact-or-throw rule, sce::Json::Parser::parse now throws with the number text and a note that the behaviour is unverified, instead of returning JsonErrorParse. The partially built tree is freed before the exception leaves the parser. The case is listed under unknown function info in TechnicalDebt.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- core/libs/prx/libSceJson2/Export.cpp | 11 +++++++++-- core/libs/tests/GuestJson.cpp | 13 ++++++++++++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 789de13a8..1ff229858 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -372,7 +372,7 @@ private: } } const double real = std::strtod(text.c_str(), nullptr); - if (!std::isfinite(real)) return false; + if (!std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); out.type = TypeReal; out.real = real; return true; @@ -734,7 +734,14 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); Node parsed{}; Parser parser(text, size); - if (!parser.Parse(parsed)) { + bool parsedOk = false; + try { + parsedOk = parser.Parse(parsed); + } catch (...) { + Clear(parsed); + throw; + } + if (!parsedOk) { Clear(parsed); return JsonErrorParse; } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 4f3f3d01b..038241cd2 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -163,10 +164,20 @@ static void ParseAndRoundTrip() { Require(Serialize(reparsed) == serialized); _ZN3sce4Json5ValueD1Ev(&reparsed); - for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru", "{} x", "01", "1e309", "[-1e309]", "{\"a\":1e400}"}) { + for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru", "{} x", "01"}) { Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } + for (const char* overflow : {"1e309", "[-1e309]", "{\"a\":[1,1e400]}"}) { + bool unverified = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, overflow, std::strlen(overflow)); + } catch (const std::runtime_error& error) { + unverified = std::string(error.what()).find("unverified") != std::string::npos; + } + Require(unverified); + Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); + } _ZN3sce4Json5ValueD1Ev(&root); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..06b6f1367 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -49,6 +49,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits +- [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws From 2cd813da978b8ad0add6250144b6ceeaf782e81e Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Tue, 6 Oct 2026 19:04:20 +0200 Subject: [PATCH 099/203] test(json2): unwind guest_json_tests on Windows The test now catches the exception that Parser::parse throws for a number that overflows a double. Windows test executables need configure_windows_unwind, as guest_png_dec_tests has, or the exception cannot be unwound and the process terminates. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- core/libs/prx/libSceJson2/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index d1cd90d94..acfda4947 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -12,6 +12,7 @@ if(BUILD_TESTING) add_test_executable(guest_json_tests ${LIBS_INCLUDE_DIR}/tests/GuestJson.cpp) target_include_directories(guest_json_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc) + configure_windows_unwind(guest_json_tests) add_test(NAME guest_json COMMAND guest_json_tests) if(WIN32) set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION From c6a91e15fd77f34e81f3a9b034bfc8852790746d Mon Sep 17 00:00:00 2001 From: Fishybeing <328519762+Fishybeing@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:14:41 +0200 Subject: [PATCH 100/203] docs: describe the barycentric fallback in TechnicalDebt accurately --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..f3e4e9ed4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -20,7 +20,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick` and `sceSystemServiceReportAbnormalTermination` return success and do nothing: the host has no auto power-off timer to reset and no system crash reporter to notify - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling -- The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). +- Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values. - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. From 177a452c6096f1f0831acd5b206fae219f65a9b5 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 6 Oct 2026 19:37:21 +0200 Subject: [PATCH 101/203] fix(shader): read relative SGPRs through a one-hot mux s_movrels and s_movrelsd_2 read the source register through a chain of selects keyed on the M0 offset, and the relative destination write is a select between that value and the old register. When the old register also appears in the chain, the NVIDIA compiler (RTX 5080, 610.74) evaluates select(a, select(b, x, y), x) as select(a || b, y, x) instead of select(a && !b, y, x), so s_movrelsd_2_b32 clobbered s13 whenever the source offset was 3. Rewriting the select, a branch with a phi, a masked xor or a small function array all fold back into the same form. Build the read as an OR of select(offset == k, s[base + k], 0) terms, with the base register used for offsets outside [1, count) as before. The result is unchanged, and the select nesting the compiler mishandles no longer forms. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../recompiler/Translation/src/IntegerInstructions.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index ba6b25f67..9897e5ee7 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -548,10 +548,11 @@ bool TranslationContext::sBitreplicateB64B32(const RdnaInstruction& inst) { } IrU32 TranslationContext::readRelativeScalar(std::uint32_t base, IrValue& offset) { - IrU32 result(ir.GetScalarReg(static_cast(base))); + IrValue& inRange = ir.ULessThan(ir.ISub(offset, ir.Constant(1u)), ir.Constant(NumScalarRegs - base - 1u)); + IrU32 result(ir.Select(inRange, ir.Constant(0u), ir.GetScalarReg(static_cast(base)))); for (std::uint32_t reg = base + 1u; reg < NumScalarRegs; ++reg) { IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base)); - result = IrU32(ir.Select(hit, ir.GetScalarReg(static_cast(reg)), result.Value())); + result = IrU32(ir.BitwiseOr(result.Value(), ir.Select(hit, ir.GetScalarReg(static_cast(reg)), ir.Constant(0u)))); } return result; } From cd0e534db3458f39bc08ca905e24b1967dc0ec4e Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 10:42:19 -0700 Subject: [PATCH 102/203] fix(relinker): reject malformed dynamic segments --- core/relinker/CMakeLists.txt | 2 + .../relinker/src/parsing/ElfReader.cpp | 13 ++- .../relinker/tests/test_dynamic_segment.py | 81 +++++++++++++++++++ .../relinker/tests/test_elf_offsets.py | 2 +- 4 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 core/relinker/relinker/tests/test_dynamic_segment.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 006738d94..90a1793d3 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -192,6 +192,8 @@ if(BUILD_TESTING) if(Python3_Interpreter_FOUND) add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $) + add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index ba8992b17..9b5ff6c1e 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -170,24 +170,31 @@ std::string ElfReader::_resolveShdrName(std::uint32_t nameOffset, const ElfHeade } std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicHeader) const { + if (!_rangeFits(dynamicHeader.Offset, dynamicHeader.FileSize, _fileBuffer.size())) { + throw RelinkerException("Dynamic segment out of bounds", dynamicHeader.Offset); + } + if (dynamicHeader.FileSize % 16 != 0) { + throw RelinkerException("Invalid dynamic segment size", dynamicHeader.Offset); + } + std::vector tags; FileByteOffset offset = dynamicHeader.Offset; const FileByteOffset end = dynamicHeader.Offset + dynamicHeader.FileSize; - while (offset + 16 <= end && offset + 16 <= _fileBuffer.size()) { + while (offset < end) { DynamicTag tag; tag.Tag = static_cast(_readU64At(offset)); tag.Value = _readU64At(offset + 0x08); if (tag.Tag == 0) { - break; + return tags; } tags.push_back(tag); offset += 16; } - return tags; + throw RelinkerException("Unterminated dynamic segment", dynamicHeader.Offset); } FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { diff --git a/core/relinker/relinker/tests/test_dynamic_segment.py b/core/relinker/relinker/tests/test_dynamic_segment.py new file mode 100644 index 000000000..830b96707 --- /dev/null +++ b/core/relinker/relinker/tests/test_dynamic_segment.py @@ -0,0 +1,81 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture as optional_plt_fixture + + +def fixture(): + image = optional_plt_fixture() + struct.pack_into(" Date: Tue, 6 Oct 2026 13:41:05 -0500 Subject: [PATCH 103/203] docs: record the FreeBSD 11.0 assumption for process-shared barrier attributes Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..4ae541006 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -77,7 +77,7 @@ Throughout the project, every function at every stage either **does exactly what - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export -- [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED throws: FreeBSD 9.0 libthr rejects it with EINVAL and 11.0 accepts it, and which one the console follows is unknown +- [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature From ad56d004f26383c83979ac2f5f01be51c7dd5ce2 Mon Sep 17 00:00:00 2001 From: Akhil Moola <24420588+ThisIsAkill@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:50:49 -0600 Subject: [PATCH 104/203] fix(agc): accept a border colour table that no address mode reads A sampler descriptor with BORDER_COLOR_TYPE 3 names an entry of the border colour table, but the hardware reads it only through an axis that addresses the border (clamp modes 4 to 7). DecodeSamplerResource threw for every table border colour, so a sampler that clamps to the edge on all axes, as PPSA21567's Armadillo Challenge 2 compute shader uses, stopped the title although its border colour is never sampled. It now throws only when an axis uses a border mode, the same rule the colour compare emulation applies, and otherwise uses a fixed Vulkan border colour. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestSamplerResource.cpp | 9 ++++++- .../tests/GuestSamplerResource.cpp | 25 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index d28297551..c2f48425e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -34,6 +34,10 @@ VkSamplerAddressMode toVkAddressMode(std::uint32_t raw) { } } +bool readsBorderColor(std::uint32_t raw) { + return raw >= 4u && raw <= 7u; +} + float toSignedLodBias(std::uint32_t raw) { const auto extended = static_cast((raw ^ 0x2000u) - 0x2000u); return static_cast(extended) / 256.0f; @@ -113,7 +117,10 @@ GuestSamplerResource DecodeSamplerResource(std::span words) case 0: border = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK; break; case 1: border = VK_BORDER_COLOR_INT_OPAQUE_BLACK; break; case 2: border = VK_BORDER_COLOR_INT_OPAQUE_WHITE; break; - default: throw std::runtime_error("AGC graphics: guest sampler descriptor uses a border color table which is not implemented"); + default: + if (readsBorderColor(clampX) || readsBorderColor(clampY) || readsBorderColor(clampZ)) throw std::runtime_error("AGC graphics: guest sampler descriptor uses a border color table which is not implemented"); + border = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK; + break; } GuestSamplerResource result{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 4af9a844c..cd7ae9889 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -189,9 +189,28 @@ void RunGuestSamplerResourceTests() { badBlendZero.blendZeroPrt = true; rejectFields(badBlendZero, "PRT blend-zero"); - Fields badBorder = base; - badBorder.borderColorType = 3; - rejectFields(badBorder, "border color table"); + Fields unusedTable = base; + unusedTable.borderColorType = 3; + const auto unread = DecodeSamplerResource(pack(unusedTable)); + Require(unread.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unread.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unread.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + "a table border colour with clamp-to-edge addressing must decode"); + for (std::uint32_t mode : {0u, 1u, 2u, 3u}) { + Fields unreadMode = base; + unreadMode.borderColorType = 3; + unreadMode.clampX = mode; + DecodeSamplerResource(pack(unreadMode)); + } + for (std::uint32_t mode : {4u, 5u, 6u, 7u}) { + for (int axis = 0; axis < 3; ++axis) { + Fields readTable = base; + readTable.borderColorType = 3; + (axis == 0 ? readTable.clampX : axis == 1 ? readTable.clampY : readTable.clampZ) = mode; + rejectFields(readTable, "border color table"); + } + } + const std::array capturedTable{0x00007092u, 0x00fff000u, 0x05000000u, 0xc0000000u}; + const auto capturedUnread = DecodeSamplerResource(capturedTable); + Require(capturedUnread.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && capturedUnread.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured table-border sampler decoded incorrectly"); Fields opaqueBlack = base; opaqueBlack.borderColorType = 1; From fc049cf1b4a091bfd49e434032ac9a750a4da179 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 13:54:12 -0500 Subject: [PATCH 105/203] test(libs): declare the write data patch function pointers with the guest ABI On Windows APS5_VABI is sysv_abi, so the exports did not convert to plain function pointers and the test did not compile. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp b/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp index 2b2fae16e..5f543c870 100644 --- a/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp +++ b/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp @@ -50,9 +50,9 @@ struct Fields { std::uint8_t writeConfirm; }; -using Writer = std::uint32_t* (*)(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); -using AddressPatch = int (*)(std::uint32_t*, std::uint64_t); -using FieldPatch = int (*)(std::uint32_t*, std::uint8_t); +using Writer = std::uint32_t* (APS5_VABI *)(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); +using AddressPatch = int (APS5_VABI *)(std::uint32_t*, std::uint64_t); +using FieldPatch = int (APS5_VABI *)(std::uint32_t*, std::uint8_t); struct Variant { Writer writer; From d155f4555c4d79b578342a14fa9b25e7c8a698a1 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:59:47 +0200 Subject: [PATCH 106/203] fix(tools): count packed MIMG variants as decoded shader instructions ImageLoadPck, ImageLoadMipPck, ImageStorePck and ImageStoreMipPck are decoded and implemented for 2- and 4-dword transfers, but the disassembler and rdna_isa.txt list them under separate _PCK2 and _PCK4 mnemonics. Existing tests in ImageLoadPacked and ImageStorePacked already cover these dword counts. --- tools/progress.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/progress.py b/tools/progress.py index 81ceeea05..a0b5ae4e4 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -53,6 +53,10 @@ OPCODE_VARIANTS = { "VMacLegacyF32": ("V_FMAC_LEGACY_F32",), "VMadLegacyF32": ("V_FMA_LEGACY_F32",), "ImageSample": ("IMAGE_SAMPLE_L", "IMAGE_SAMPLE_B", "IMAGE_SAMPLE_C_LZ", "IMAGE_SAMPLE_L_O", "IMAGE_SAMPLE_D_CL_O"), + "ImageLoadPck": ("IMAGE_LOAD_PCK2", "IMAGE_LOAD_PCK4"), + "ImageLoadMipPck": ("IMAGE_LOAD_MIP_PCK2", "IMAGE_LOAD_MIP_PCK4"), + "ImageStorePck": ("IMAGE_STORE_PCK2", "IMAGE_STORE_PCK4"), + "ImageStoreMipPck": ("IMAGE_STORE_MIP_PCK2", "IMAGE_STORE_MIP_PCK4"), } REPORT_ROWS = 100 PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30 From c70c2b35261b16a56863a7f94a52fcb8a4cba45b Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 12:23:04 -0700 Subject: [PATCH 107/203] fix(libc): honor sscanf_s count conversion lengths --- core/libs/prx/libc/CMakeLists.txt | 9 ++++ core/libs/prx/libc/src/Formatting.cpp | 12 ++++- core/libs/tests/GuestScanCount.cpp | 70 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestScanCount.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 575971f71..951173fe7 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -45,6 +45,15 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(guest_scan_count_tests ${LIBS_INCLUDE_DIR}/tests/GuestScanCount.cpp) + target_include_directories(guest_scan_count_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_scan_count_tests PRIVATE libc) + configure_windows_unwind(guest_scan_count_tests) + add_test(NAME guest_scan_count COMMAND guest_scan_count_tests) + if(WIN32) + set_tests_properties(guest_scan_count PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_access_tests ${LIBS_INCLUDE_DIR}/tests/GuestAccess.cpp) target_include_directories(guest_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_access_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0cc9b6e63..ffff79f04 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -76,7 +76,17 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n } ++cursor; if (conversion == 'n') { - if (!suppress) *static_cast(nextPointer()) = static_cast(input - buffer); + if (!suppress) { + void* target = nextPointer(); + const auto count = input - buffer; + if (length == "hh") *static_cast(target) = static_cast(count); + else if (length == "h") *static_cast(target) = static_cast(count); + else if (length.empty()) *static_cast(target) = static_cast(count); + else if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") { + const std::int64_t value = count; + std::memcpy(target, &value, sizeof(value)); + } else throw std::invalid_argument("Invalid scan count length"); + } continue; } if (conversion != 'c' && conversion != '[') { diff --git a/core/libs/tests/GuestScanCount.cpp b/core/libs/tests/GuestScanCount.cpp new file mode 100644 index 000000000..f77e73b62 --- /dev/null +++ b/core/libs/tests/GuestScanCount.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sscanf_s_nid_postfix(const char*, const char*, ...); + +static int failures = 0; + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + ++failures; + } +} + +template static void CheckCount(const char* modifier, int count) { + struct alignas(8) Destination { + TValue value; + std::array guard; + } destination{static_cast(-1), {}}; + destination.guard.fill(0xa5); + const auto guard = destination.guard; + const std::string input(static_cast(count), 'x'); + const std::string format = input + "%" + modifier + "n"; + const int assigned = sscanf_s_nid_postfix(input.c_str(), format.c_str(), &destination.value); + if (destination.value != count || destination.guard != guard || assigned != 0) { + std::fprintf(stderr, "%%%sn after %d bytes: count=%lld, guard=%s, assignments=%d\n", + modifier, count, static_cast(destination.value), + destination.guard == guard ? "unchanged" : "overwritten", assigned); + ++failures; + } +} + +int main() { + for (const int count : {0, 3, 127}) { + CheckCount("hh", count); + CheckCount("h", count); + CheckCount("", count); + CheckCount("l", count); + CheckCount("ll", count); + CheckCount("j", count); + CheckCount("z", count); + CheckCount("t", count); + } + CheckCount("h", 257); + CheckCount("l", 257); + + int number = -1; + std::int64_t before = -1; + std::int64_t after = -1; + char word[4] = {}; + Require(sscanf_s_nid_postfix(" 12 abc!", "%ln%d %3[a-z]%jn!", &before, &number, + word, 4u, &after) == 2, "Count conversions must not increase assignments"); + Require(before == 0 && number == 12 && std::strcmp(word, "abc") == 0 && after == 7, + "Counts must include leading whitespace and earlier conversions"); + + after = -1; + Require(sscanf_s_nid_postfix("123x", "%*d%tn", &after) == 0 && after == 3, + "Count must include suppressed input without consuming a capacity argument"); + after = -1; + Require(sscanf_s_nid_postfix("x", "%d%lln", &number, &after) == 0 && after == -1, + "Matching failure must leave a later count untouched"); + Require(sscanf_s_nid_postfix("", "%d%lln", &number, &after) == EOF && after == -1, + "Input failure must leave a later count untouched"); + return failures == 0 ? 0 : 1; +} From b12ef2cf2078ce1164efd275e937cc728e5c34d2 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch Date: Tue, 6 Oct 2026 16:48:10 -0300 Subject: [PATCH 108/203] fix(relinker): fall back to unqualified lookup for unmatched import modules A qualified import whose module name matches no DT_NEEDED entry now uses the unqualified search order instead of failing with "Import module is not a dependency". Restore the Linux rejection of duplicate exports within one guest module. Co-Authored-By: Claude Opus 5.5 --- core/relinker/domain/include/domain/ImportModule.hpp | 3 +-- core/relinker/relinker/src/guest/GuestModuleBuilder.cpp | 4 +++- core/relinker/relinker/tests/test_windows_import_modules.py | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index 8da345c27..81b0c3b2b 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -44,8 +44,7 @@ inline std::string ImportModule(const std::string& symbol, const std::map GuestModuleBuilder::Build(const std::filesystem::path for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; - if (std::find(providers.begin(), providers.end(), images.size()) == providers.end()) providers.push_back(images.size()); + const bool repeated = std::find(providers.begin(), providers.end(), images.size()) != providers.end(); + if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + path.string() + " and " + path.string()); + if (!repeated) providers.push_back(images.size()); if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end()); } std::vector codeHeaders; diff --git a/core/relinker/relinker/tests/test_windows_import_modules.py b/core/relinker/relinker/tests/test_windows_import_modules.py index 8b25b1ee9..af5ea7894 100644 --- a/core/relinker/relinker/tests/test_windows_import_modules.py +++ b/core/relinker/relinker/tests/test_windows_import_modules.py @@ -125,6 +125,12 @@ def main(): run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) assert run.returncode == 22, (run.returncode, run.stdout, run.stderr) + result, output = convert('unmatched-module', 'libFoo2.prx', module_name='libFoo', provider_name='libFoo2.prx') + assert result.returncode == 0, result.stderr + if os.name == 'nt': + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 11, (run.returncode, run.stdout, run.stderr) + for symbol, message in [('shared#A#C', 'Unknown import module ID'), ('shared#A#', 'Invalid qualified import'), ('shared#A#?', 'Invalid import module ID'), From 9edc22a4a930287cd650f739e0659872b168b2a3 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:40:39 +0200 Subject: [PATCH 109/203] feat(libSceMsgDialog.native): implement sceMsgDialogGetStatus --- core/libs/prx/libSceMsgDialog.native/Export.cpp | 6 ++---- core/libs/tests/GuestMsgDialogNative.cpp | 6 ++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 678c37088..edb76df0f 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -83,10 +83,8 @@ int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } -APS5_EXPORT("CWVW78Qc3fI", sceMsgDialogUnknown00); -int APS5_VABI sceMsgDialogUnknown00(void) { - NotImplemented_nid_no_patch("CWVW78Qc3fI"); - return 0; +int APS5_VABI sceMsgDialogGetStatus(void) { + return g_status.load(); } } diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 34f98d06b..dd43d10b4 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI sceMsgDialogInitialize(void); int APS5_VABI sceMsgDialogOpen(const void* param); +int APS5_VABI sceMsgDialogGetStatus(void); int APS5_VABI sceMsgDialogUpdateStatus(void); int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result); int APS5_VABI sceMsgDialogClose(void); @@ -31,19 +32,23 @@ int main() { Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == 0); + Require(sceMsgDialogGetStatus() == 0); Require(sceMsgDialogInitialize() == 0); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); + Require(sceMsgDialogGetStatus() == kStatusInitialized); Require(sceMsgDialogProgressBarSetValue(0, 50) == kErrNotRunning); Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogUpdateStatus() == kStatusFinished); + Require(sceMsgDialogGetStatus() == kStatusFinished); Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusFinished); + Require(sceMsgDialogGetStatus() == kStatusFinished); MsgDialogResult result{}; result.button_id = -1; @@ -53,4 +58,5 @@ int main() { Require(sceMsgDialogTerminate() == 0); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogGetStatus() == 0); } From 157aac7511532894a1ab4c0fb6dd9170fc2e6bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 22:12:08 +0200 Subject: [PATCH 110/203] docs(contributing): shader PRs say where instruction semantics come from Co-Authored-By: Claude --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c12a36055..212bdb9a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ - Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits. - Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)). +- Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md). - A silent stub is allowed only when it unblocks a title and only affects the UI; add it to [silent stubs](docs/dev/TechnicalDebt.md#silent-stubs). - Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running. - Implement the general behaviour of a function, not what one title happens to need. From cf0955f277c79b269e7aa4114101c67ab0367d6e Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 15:13:58 -0500 Subject: [PATCH 111/203] docs: keep the Http2 cookie box entry beside the other libSceHttp2 entries Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ca22769ce..30f70d765 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -112,6 +112,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature +- [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceHttpSetRequestStatusCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - no reference implements it (shadPS4 only logs and returns 0); when the callback runs and what status it gets is unknown - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature @@ -132,7 +133,6 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown -- [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard ### Functional From e419513f48512540ba00c5d616466fe73c2ab9d2 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:59:54 +0200 Subject: [PATCH 112/203] fix(agc): don't create the Vulkan instance while SDL creates the Vulkan window SDL_CreateWindow with SDL_WINDOW_VULKAN loads and queries the Vulkan loader while the render thread can be inside vkCreateInstance. On Windows the two together intermittently made the NVIDIA ICD fail instance creation (-3) and the loader return VK_ERROR_INCOMPATIBLE_DRIVER. libSceAgcDriver now exports a loader lock that the VulkanDevice constructor and DisplayWindow::create both hold. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/include/Driver.hpp | 2 ++ .../Execution/src/Driver/EntryPoints.cpp | 14 ++++++++++++++ .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 5 +++++ core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp | 3 +++ 4 files changed, 24 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 665b5d159..1f643db73 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -11,6 +11,8 @@ void Submit(const Packet* packet, std::uint32_t queue); } extern "C" void AgcDriverWaitIdle_nid_postfix(); +extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); +extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); extern "C" void AgcDriverShutdown_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 9b622dbff..91e589164 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include namespace AgcDriver { @@ -56,6 +57,19 @@ extern "C" void AgcDriverWaitIdle_nid_postfix() try { LibcAwaitExit_nid_postfix(); } +static std::mutex& VulkanLoaderMutex() { + static std::mutex mutex; + return mutex; +} + +extern "C" void AgcDriverLockVulkanLoader_nid_postfix() { + VulkanLoaderMutex().lock(); +} + +extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix() { + VulkanLoaderMutex().unlock(); +} + extern "C" void AgcDriverShutdown_nid_postfix() { AgcDriver::Shutdown(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5cadb337f..a56e3341e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "BdaAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" @@ -570,6 +571,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u if (state->instanceProc == nullptr) { throw std::runtime_error("Vulkan loader: vkGetInstanceProcAddr missing"); } + AgcDriverLockVulkanLoader_nid_postfix(); + struct LoaderUnlock { + ~LoaderUnlock() { AgcDriverUnlockVulkanLoader_nid_postfix(); } + } loaderUnlock; VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; application.pApplicationName = "AnyPS5 libSceAgcDriver"; application.apiVersion = VK_API_VERSION_1_1; diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 6717efe30..1b60c53ec 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -1,5 +1,6 @@ #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "SDL_vulkan.h" @@ -45,7 +46,9 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight const auto initialSize = AgcDriver::ComputeContainSize_nid_postfix(sourceWidth, sourceHeight, boundsWidth, boundsHeight, true); require(initialSize.width >= DisplayWindowMinimumWidth && initialSize.height >= DisplayWindowMinimumHeight, "initial window extent is smaller than the minimum"); const auto title = GetAppTitle_nid_postfix(); + AgcDriverLockVulkanLoader_nid_postfix(); window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); installSubclass(); From 81a332c148474ce43f0ee4997ed3369fb5abc9d9 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:12:35 +0200 Subject: [PATCH 113/203] feat(libSceNgs2.native): mastering voice gain parameter Mastering voice param 0x30000004 sets a full-band and an LFE level. They scale the mastering voice when it is mixed into the render buffer, the LFE level on channel 3 of 6- and 8-channel voices. Non-finite levels are invalid arguments. Co-Authored-By: Claude Opus 5.5 --- .../libSceNgs2.native/include/Ngs2Types.hpp | 8 +++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 2 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 4 +- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 9 +++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 66 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 89 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index e7d5fd04c..0c86e9b8b 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -50,6 +50,7 @@ static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH = 0x10000005; static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER = 0x1000000a; static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP = 0x20000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; +static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_GAIN = 0x30000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; @@ -402,6 +403,13 @@ struct Ngs2MasteringVoiceSetupParam { std::uint32_t flags; }; +struct Ngs2MasteringVoiceGainParam { + Ngs2VoiceParamHeader header; + float fbw_level; + float lfe_level; +}; +static_assert(sizeof(Ngs2MasteringVoiceGainParam) == 16); + struct Ngs2MasteringVoiceOutputParam { Ngs2VoiceParamHeader header; std::uint32_t output_id; diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 1aa4d4283..f49ba05ef 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -101,6 +101,8 @@ struct Ngs2Voice { std::vector> matrices; std::vector filters; std::uint32_t outputId = 0; + float fbwLevel = 1.0f; + float lfeLevel = 1.0f; std::vector userFx; std::vector samples; bool rendering = false; diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 4d0f5e394..8f307efb8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -228,7 +228,9 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo if (voice->rack->rackId != SCE_NGS2_RACK_ID_MASTERING || !voice->hasSamples) continue; auto& mix = OutputMix(mixes, bufferInfo, numBufferInfo, *voice, grain); for (std::uint32_t channel = 0; channel < voice->channels; channel++) { - for (std::uint32_t i = 0; i < grain; i++) mix[i * voice->channels + channel] += voice->samples[channel * grain + i]; + const bool lfe = channel == 3 && (voice->channels == 6 || voice->channels == 8); + const float level = lfe ? voice->lfeLevel : voice->fbwLevel; + for (std::uint32_t i = 0; i < grain; i++) mix[i * voice->channels + channel] += voice->samples[channel * grain + i] * level; } } for (std::uint32_t i = 0; i < numBufferInfo; i++) { diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 9afecf10c..37e571613 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -49,6 +49,8 @@ void Ngs2Voice::ResetSetup() { std::fill(ports.begin(), ports.end(), Ngs2Port{}); for (auto& matrix : matrices) matrix.clear(); filters.clear(); + fbwLevel = 1.0f; + lfeLevel = 1.0f; channels = 0; sampleRate = 0; waveformType = 0; @@ -257,6 +259,13 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { SetupMixer(voice, ParamAs(param).num_io_channels); return; } + if (param.id == SCE_NGS2_MASTERING_VOICE_PARAM_GAIN) { + const auto& gain = ParamAs(param); + if (!std::isfinite(gain.fbw_level) || !std::isfinite(gain.lfe_level)) APS5_INVALID_ARG_EX; + voice.fbwLevel = gain.fbw_level; + voice.lfeLevel = gain.lfe_level; + return; + } if (param.id == SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT) { voice.outputId = ParamAs(param).output_id; return; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 5250c7eee..6935308a8 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -199,6 +200,70 @@ static void TestUserData() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestMasteringGain() { + const auto system = CreateSystem(); + const auto stereo = Mastering(system, 2); + const auto surround = Mastering(system, 6); + const auto quad = Mastering(system, 4); + Control(stereo, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 1.0f}); + Control(surround, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 0.25f}); + Control(surround, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 1}); + Control(quad, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 0.25f}); + Control(quad, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 2}); + const std::vector pcm(Grain, 16384); + const auto toStereo = Sampler(system, pcm, 0); + const float stereoLevels[2] = {1.0f, 0.5f}; + Control(toStereo, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 2, stereoLevels}); + Control(toStereo, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(toStereo, stereo); + Event(toStereo, SCE_NGS2_VOICE_EVENT_PLAY); + const auto toSurround = Sampler(system, pcm, 0); + const float surroundLevels[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + Control(toSurround, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 6, surroundLevels}); + Control(toSurround, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(toSurround, surround); + Event(toSurround, SCE_NGS2_VOICE_EVENT_PLAY); + const auto toQuad = Sampler(system, pcm, 0); + Control(toQuad, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 4, surroundLevels}); + Control(toQuad, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(toQuad, quad); + Event(toQuad, SCE_NGS2_VOICE_EVENT_PLAY); + bool rejected = false; + try { Control(stereo, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, NAN, 1.0f}); } catch (const std::exception&) { rejected = true; } + Require(rejected); + + std::vector outStereo(Grain * 2, -1.0f); + std::vector outSurround(Grain * 6, -1.0f); + std::vector outQuad(Grain * 4, -1.0f); + const Ngs2RenderBufferInfo info[3] = { + {outStereo.data(), outStereo.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}, + {outSurround.data(), outSurround.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 6}, + {outQuad.data(), outQuad.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4}, + }; + Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(outStereo[i * 2] == 0.25f && outStereo[i * 2 + 1] == 0.125f); + for (std::uint32_t channel = 0; channel < 6; channel++) Require(outSurround[i * 6 + channel] == (channel == 3 ? 0.125f : 0.25f)); + for (std::uint32_t channel = 0; channel < 4; channel++) Require(outQuad[i * 4 + channel] == 0.25f); + } + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + + const auto loudSystem = CreateSystem(); + const auto loud = Mastering(loudSystem, 1); + Control(loud, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 2.0f, 2.0f}); + const std::vector loudPcm(Grain * 2, 24576); + const auto toLoud = Sampler(loudSystem, loudPcm, 0); + Patch(toLoud, loud); + Event(toLoud, SCE_NGS2_VOICE_EVENT_PLAY); + auto clipped = RenderI16(loudSystem); + for (std::int16_t sample : clipped) Require(sample == 32767); + Control(loud, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 1, 0}); + Event(loud, SCE_NGS2_VOICE_EVENT_PLAY); + auto reset = RenderI16(loudSystem); + for (std::int16_t sample : reset) Require(sample == 24576); + Require(sceNgs2SystemDestroy(loudSystem, nullptr) == SCE_NGS2_OK); +} + static void TestLock() { const auto system = CreateSystem(); Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); @@ -242,6 +307,7 @@ int main() { TestSubmixerMatrix(); TestSampleRate(); TestUserData(); + TestMasteringGain(); TestLock(); TestAllocator(); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..cb574c2a3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -99,6 +99,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one +- [NGS2 mastering voice gain](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info From 687be1fbc898ee127e232cae8a93526b23afb8c7 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:21:23 +0200 Subject: [PATCH 114/203] feat(libSceNgs2): build libSceNgs2 from the libSceNgs2.native implementation PPSA22520 lists libSceNgs2.prx, not libSceNgs2.native.prx, among its needed modules, so its NGS2 imports did not resolve against the dummy module. Both modules are now built by add_sce_ngs2_library from the same sources, as libSceFont and libSceFont.native share add_sce_font_library, and the render tests also run against libSceNgs2. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 22 ++---------------- .../prx/libSceNgs2.native/Ngs2Library.cmake | 23 +++++++++++++++++++ core/libs/prx/libSceNgs2/CMakeLists.txt | 20 ++++++++++++++++ core/libs/prx/libSceNgs2/Export.cpp | 10 -------- 4 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/Ngs2Library.cmake create mode 100644 core/libs/prx/libSceNgs2/CMakeLists.txt delete mode 100644 core/libs/prx/libSceNgs2/Export.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index a2d50f9b8..b9ed2af05 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -1,23 +1,5 @@ -add_library(libSceNgs2.native SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/src/Atrac9.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Custom.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Rack.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Render.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/System.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Voice.cpp -) -target_include_directories(libSceNgs2.native PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) -target_link_libraries(libSceNgs2.native PRIVATE atrac9) -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_options(libSceNgs2.native PRIVATE LINKER:--exclude-libs,ALL) -endif() -set_target_properties(libSceNgs2.native PROPERTIES - CXX_EXTENSIONS OFF - CXX_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN ON -) -configure_windows_unwind(libSceNgs2.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) +add_sce_ngs2_library(libSceNgs2.native) if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake new file mode 100644 index 000000000..5f8a9df33 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -0,0 +1,23 @@ +function(add_sce_ngs2_library target) + set(ngs2Dir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${ngs2Dir}/src/Atrac9.cpp + ${ngs2Dir}/src/Custom.cpp + ${ngs2Dir}/src/Rack.cpp + ${ngs2Dir}/src/Render.cpp + ${ngs2Dir}/src/System.cpp + ${ngs2Dir}/src/Unimplemented.cpp + ${ngs2Dir}/src/Voice.cpp + ) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(${target} PRIVATE atrac9) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(${target} PRIVATE LINKER:--exclude-libs,ALL) + endif() + set_target_properties(${target} PROPERTIES + CXX_EXTENSIONS OFF + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt new file mode 100644 index 000000000..8e57a8733 --- /dev/null +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -0,0 +1,20 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/Ngs2Library.cmake) +add_sce_ngs2_library(libSceNgs2) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) + target_include_directories(ngs2_core_render_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(ngs2_core_render_tests PRIVATE libSceNgs2 atrac9 libkernel libc) + configure_windows_unwind(ngs2_core_render_tests) + add_test(NAME ngs2_core_render COMMAND ngs2_core_render_tests) + if(WIN32) + set_tests_properties(ngs2_core_render PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNgs2/Export.cpp b/core/libs/prx/libSceNgs2/Export.cpp deleted file mode 100644 index fb34206c2..000000000 --- a/core/libs/prx/libSceNgs2/Export.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -APS5_DUMMY_FUN - -} From 1f53f885d42bdfec2b8a2469e3d8861f0b9fbe49 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:26:54 +0200 Subject: [PATCH 115/203] feat(libSceAgcDriver): implement sceAgcDriverGetResourceRegistrationMaxNameLength The query writes the maximum resource name length (0xfc) through its output pointer and returns 0, as prosper does from a title's registration configuration. The parameter follows the SDK wrapper sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*). Resource registration itself stays unavailable. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgcDriver/Resource/src/Registration.cpp | 7 +++++++ core/libs/prx/libSceAgcDriver/Unimplemented.cpp | 5 ----- core/libs/tests/GuestAgcDriverResources.cpp | 3 +++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 4e5eaa6ab..ff798fca2 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -7,6 +7,7 @@ // Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign. static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast(0x8A6C9018); +static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc; extern "C" { @@ -69,6 +70,12 @@ int APS5_VABI sceAgcDriverGetDefaultOwner(uint32_t* owner_handle) { return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; } +int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength(uint32_t* max_length) { + if (max_length == nullptr) APS5_INVALID_ARG_EX; + *max_length = RESOURCE_REGISTRATION_MAX_NAME_LENGTH; + return 0; +} + int APS5_VABI sceAgcDriverGetOwnerName(uint32_t owner_handle, const char** name) { (void)owner_handle; (void)name; diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 8add7f045..7b2d31ea6 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -5,11 +5,6 @@ extern "C" { -int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index d476831c1..eb97484c7 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(std::uint32_t); int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t, const void*); int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t); int APS5_VABI sceAgcDriverGetDefaultOwner(std::uint32_t*); +int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength(std::uint32_t*); int APS5_VABI sceAgcDriverGetOwnerName(std::uint32_t, const char**); int APS5_VABI sceAgcDriverGetResourceName(std::uint32_t, const char**); int APS5_VABI sceAgcDriverGetResourceType(std::uint32_t, std::uint32_t*); @@ -37,6 +38,8 @@ int main() { Require(sceAgcDriverRegisterGdsResource(&resource, owner, 0u, 64u, "gds", 1u, 2u) == Unavailable); Require(owner == 7u && resource == 9u); Require(sceAgcDriverGetDefaultOwner(&owner) == Unavailable && owner == 7u); + std::uint32_t maxNameLength = 8u; + Require(sceAgcDriverGetResourceRegistrationMaxNameLength(&maxNameLength) == 0 && maxNameLength == 0xfcu); const char* name = "unchanged"; Require(sceAgcDriverGetOwnerName(owner, &name) == Unavailable); Require(sceAgcDriverGetResourceName(resource, &name) == Unavailable); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..8f115f126 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -130,7 +130,7 @@ Throughout the project, every function at every stage either **does exactly what - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) -- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified +- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown ### Functional From 47fa19e15cdb9d697084bc298a9ccca87f99d86f Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 17:31:44 -0300 Subject: [PATCH 116/203] feat(libSceAgc): implement the async COND_EXEC patches sceAgcAsyncCondExecPatchSetCommandAddress and sceAgcAsyncCondExecPatchSetEnd patch the COND_EXEC packet like their graphics counterparts, since sceAgcAcbCondExec writes the same packet as sceAgcDcbCondExec. Both variants share one implementation, as the REWIND patches do. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/CMakeLists.txt | 1 + .../libs/prx/libSceAgc/Patch/src/CondExec.cpp | 38 +++++++----- .../prx/libSceAgc/tests/CondExecPatch.cpp | 60 ++++++++++++------- docs/dev/TechnicalDebt.md | 1 + 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index d3bb48f0b..ac7c86750 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -291,6 +291,7 @@ add_test_executable(agc_cond_exec_patch_tests EXCLUDE_FROM_ALL Command/src/Memory.cpp Command/src/Control.cpp DcbFlow/src/Sync.cpp + Acb/src/Sync.cpp Patch/src/CondExec.cpp ) target_include_directories(agc_cond_exec_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp index bae2206ad..7fff4987c 100644 --- a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp @@ -7,40 +7,46 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -extern "C" { +namespace { -int APS5_VABI sceAgcCondExecPatchSetCommandAddress(uint32_t* cmd, const volatile uint32_t* command) { - Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__); +int SetCommandAddress(std::uint32_t* cmd, const volatile std::uint32_t* command, const char* function) { + Agc::Command::ValidatePacket(cmd, 0x22u, 5, function); const auto address = reinterpret_cast(command); - Agc::Command::CheckAddress(address, 4, __func__); + Agc::Command::CheckAddress(address, 4, function); cmd[1] = (cmd[1] & 3u) | (static_cast(address) & ~3u); cmd[2] = static_cast(address >> 32u); return 0; } -int APS5_VABI sceAgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer) { - Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__); +int SetEnd(std::uint32_t* cmd, const volatile std::uint32_t* buffer, const char* function) { + Agc::Command::ValidatePacket(cmd, 0x22u, 5, function); const auto end = reinterpret_cast(buffer); const auto packetEnd = reinterpret_cast(cmd + 5); - Agc::Command::CheckAddress(end, 4, __func__); + Agc::Command::CheckAddress(end, 4, function); const auto numDwords = (end - packetEnd) / sizeof(std::uint32_t); - Agc::Command::Require(numDwords <= 0x3fffu, __func__, "conditional execution range is too large"); + Agc::Command::Require(numDwords <= 0x3fffu, function, "conditional execution range is too large"); cmd[4] = (cmd[4] & ~0x3fffu) | static_cast(numDwords); return 0; } +} + +extern "C" { + +int APS5_VABI sceAgcCondExecPatchSetCommandAddress(uint32_t* cmd, const volatile uint32_t* command) { + return SetCommandAddress(cmd, command, __func__); +} + +int APS5_VABI sceAgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer) { + return SetEnd(cmd, buffer, __func__); +} + int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress(std::uint32_t* cmd, const volatile std::uint32_t* command) { - (void)cmd; - (void)command; - NotImplemented_nid_no_patch(__func__); - return 0; + return SetCommandAddress(cmd, command, __func__); } int APS5_VABI sceAgcAsyncCondExecPatchSetEnd(std::uint32_t* cmd, const volatile std::uint32_t* buffer) { - (void)cmd; - (void)buffer; - NotImplemented_nid_no_patch(__func__); - return 0; + return SetEnd(cmd, buffer, __func__); } } diff --git a/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp index f338beea7..762f40b1a 100644 --- a/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp +++ b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp @@ -9,14 +9,26 @@ extern "C" { std::uint32_t* APS5_VABI sceAgcDcbCondExec(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcAcbCondExec(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t); int APS5_VABI sceAgcCondExecPatchSetCommandAddress(std::uint32_t*, const volatile std::uint32_t*); int APS5_VABI sceAgcCondExecPatchSetEnd(std::uint32_t*, const volatile std::uint32_t*); +int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress(std::uint32_t*, const volatile std::uint32_t*); +int APS5_VABI sceAgcAsyncCondExecPatchSetEnd(std::uint32_t*, const volatile std::uint32_t*); } namespace { constexpr std::uint32_t Sentinel = 0xabcdef01u; +using CondExecWriter = std::uint32_t* (APS5_VABI *)(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t); +using PatchFunction = int (APS5_VABI *)(std::uint32_t*, const volatile std::uint32_t*); + +struct Variant { + CondExecWriter write; + PatchFunction setEnd; + PatchFunction setCommandAddress; +}; + void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } @@ -41,9 +53,9 @@ struct Storage { std::uint32_t condition = 1; std::uint32_t* packet = nullptr; - Storage() { + explicit Storage(CondExecWriter write) { words.fill(Sentinel); - packet = sceAgcDcbCondExec(&buffer, &condition, 3); + packet = write(&buffer, &condition, 3); check(packet == words.data() && packet[0] == 0xc0032200u && packet[4] == 3, "unexpected COND_EXEC packet"); } }; @@ -53,51 +65,57 @@ std::uintptr_t endAfter(const std::uint32_t* packet, std::uintptr_t numDwords) { } template -void expectUnchanged(TAction action) { - Storage storage; +void expectUnchanged(const Variant& variant, TAction action) { + Storage storage(variant.write); const auto before = storage.words; expectFailure([&] { action(storage.packet); }); check(storage.words == before, "failed patch modified the packet"); } -void testSetEnd() { +void testSetEnd(const Variant& variant) { + const auto setEnd = variant.setEnd; for (const std::uintptr_t numDwords : {0u, 1u, 7u, 0x3fffu}) { - Storage storage; + Storage storage(variant.write); const auto before = storage.words; - check(sceAgcCondExecPatchSetEnd(storage.packet, at(endAfter(storage.packet, numDwords))) == 0, "SetEnd failed"); + check(setEnd(storage.packet, at(endAfter(storage.packet, numDwords))) == 0, "SetEnd failed"); check(storage.packet[4] == numDwords, "SetEnd wrote the wrong dword count"); check(std::equal(before.begin(), before.begin() + 4, storage.words.begin()), "SetEnd modified other packet words"); check(std::equal(before.begin() + 5, before.end(), storage.words.begin() + 5), "SetEnd modified following words"); } - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, at(endAfter(packet, 0x4000u))); }); - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, packet + 4); }); - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, nullptr); }); - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, at(endAfter(packet, 2) + 2)); }); - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet + 1, at(endAfter(packet, 2))); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, at(endAfter(packet, 0x4000u))); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, packet + 4); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, nullptr); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, at(endAfter(packet, 2) + 2)); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet + 1, at(endAfter(packet, 2))); }); } -void testSetCommandAddress() { +void testSetCommandAddress(const Variant& variant) { + const auto setCommandAddress = variant.setCommandAddress; for (const std::uintptr_t address : {std::uintptr_t{0x0000123456789abcu}, std::uintptr_t{4u}, std::uintptr_t{0x0000fffffffffffcu}}) { - Storage storage; + Storage storage(variant.write); + storage.packet[1] |= 3u; const auto before = storage.words; - check(sceAgcCondExecPatchSetCommandAddress(storage.packet, at(address)) == 0, "SetCommandAddress failed"); - check(storage.packet[1] == static_cast(address) && storage.packet[2] == static_cast(address >> 32u), "SetCommandAddress wrote the wrong address"); + check(setCommandAddress(storage.packet, at(address)) == 0, "SetCommandAddress failed"); + check(storage.packet[1] == (static_cast(address) | 3u) && storage.packet[2] == static_cast(address >> 32u), "SetCommandAddress wrote the wrong address"); check(storage.packet[0] == before[0] && storage.packet[3] == before[3] && storage.packet[4] == before[4], "SetCommandAddress modified other packet words"); check(std::equal(before.begin() + 5, before.end(), storage.words.begin() + 5), "SetCommandAddress modified following words"); } - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet, nullptr); }); - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet, at(0x1002u)); }); - expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet + 1, at(0x1000u)); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setCommandAddress(packet, nullptr); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setCommandAddress(packet, at(0x1002u)); }); + expectUnchanged(variant, [=](std::uint32_t* packet) { setCommandAddress(packet + 1, at(0x1000u)); }); } } int main() { try { - testSetEnd(); - testSetCommandAddress(); + for (const Variant& variant : {Variant{sceAgcDcbCondExec, sceAgcCondExecPatchSetEnd, sceAgcCondExecPatchSetCommandAddress}, + Variant{sceAgcAcbCondExec, sceAgcAsyncCondExecPatchSetEnd, sceAgcAsyncCondExecPatchSetCommandAddress}}) { + testSetEnd(variant); + testSetCommandAddress(variant); + } std::puts("AGC COND_EXEC patch tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..7ddfc7644 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -61,6 +61,7 @@ Throughout the project, every function at every stage either **does exactly what - [dbOlWdppb4o](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [V++UgBtQhn0](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name - [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `V++UgBtQhn0`; prosper returns header + 2 for both types from that address function +- [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [gQkqkLttcpw](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature From 34689f39ca46a8957af935a95f1934a7bfa7e660 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 13:19:56 -0700 Subject: [PATCH 117/203] fix(libc): preserve guest stdio bytes on Windows --- core/libs/prx/libc/src/FileAndHeap.cpp | 12 +++++- core/libs/tests/GuestStandardStreams.cpp | 54 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 80f1716a1..39ac79d85 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -12,6 +12,14 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +static std::string NativeFileMode(const char* mode) { + std::string result(mode); +#ifdef _WIN32 + if (!result.empty() && result.find('b') == std::string::npos) result.insert(1, 1, 'b'); +#endif + return result; +} + extern "C" { [[noreturn]] void APS5_VABI _ZSt11_Xbad_allocv_nid_postfix(); @@ -49,7 +57,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!valid) { errno = 22; return nullptr; } try { const auto path = *filename ? ResolvePath_nid_no_patch(filename).string() : std::string{}; - if (stream->Reopen(path.c_str(), mode)) return stream; + if (stream->Reopen(path.c_str(), NativeFileMode(mode).c_str())) return stream; const int error = errno; if (stream->IsDynamic()) delete stream; errno = error; @@ -62,7 +70,7 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (!filename || !mode) throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_NULL_ARG); const std::filesystem::path fpath = ResolvePath_nid_no_patch(filename); const auto abs_path = fpath.string(); - std::unique_ptr handle(std::fopen(abs_path.c_str(), mode), std::fclose); + std::unique_ptr handle(std::fopen(abs_path.c_str(), NativeFileMode(mode).c_str()), std::fclose); if (!handle) { const int error = errno; if (error == ENOENT) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index ee322b0d4..ad9a64339 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -6,11 +6,16 @@ #include #include #include +#include +#include #include #ifndef _WIN32 #include #endif extern "C" { +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +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*); int APS5_VABI fseeko_nid_postfix(FileStream*, std::int64_t, int); std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); @@ -76,6 +81,54 @@ static int APS5_VABI FormatString(char* buffer, const char* format, ...) { #endif return result; } +static bool CheckFileBytes(const std::string& filename, const std::string& expected, const char* mode, bool reopen) { + std::ifstream file(filename, std::ios::binary); + const std::string actual((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (actual == expected) return true; + std::fprintf(stderr, "%s %s: expected %zu raw bytes, received %zu\n", reopen ? "freopen" : "fopen", mode, expected.size(), actual.size()); + return false; +} + +static bool CheckBinaryModes() { + const auto directory = "anyps5-byte-stream-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(std::filesystem::create_directory(directory)); + const auto filename = directory + "/bytes"; + const std::string original("A\r\n\x1a" "B\0C", 7); + const std::string written("D\n\x1a" "E\0F", 6); + const char* modes[] = {"r", "r+", "w", "w+", "a", "a+", "rb", "rb+", "r+b", "wb", "wb+", "w+b", "ab", "ab+", "a+b"}; + bool correct = true; + for (const auto* mode : modes) { + for (const bool reopen : {false, true}) { + { std::ofstream seed(filename, std::ios::binary | std::ios::trunc); seed.write(original.data(), original.size()); Require(seed.good()); } + FileStream redirected(std::tmpfile()); + FileStream* stream = reopen ? freopen_nid_postfix(filename.c_str(), mode, &redirected) : fopen_nid_postfix(filename.c_str(), mode); + Require(stream != nullptr); + const bool update = std::strchr(mode, '+') != nullptr; + if (*mode == 'r' || (*mode == 'a' && update)) { + Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); + char bytes[16]{}; + const auto count = fread_nid_postfix(bytes, 1, sizeof(bytes), stream); + const bool matches = count == original.size() && std::memcmp(bytes, original.data(), original.size()) == 0; + if (!matches) std::fprintf(stderr, "%s %s: expected 7 input bytes, received %zu\n", reopen ? "freopen" : "fopen", mode, count); + correct &= matches; + Require(fseeko_nid_postfix(stream, 3, SEEK_SET) == 0); + correct &= fgetc_nid_postfix(stream) == 0x1a; + } + std::string expected = original; + if (*mode == 'w' || *mode == 'a' || update) { + Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); + Require(fwrite_nid_postfix(written.data(), 1, written.size(), stream) == written.size()); + expected = *mode == 'w' ? written : *mode == 'a' ? original + written : written + original.substr(written.size()); + } + Require(fclose_nid_postfix(stream) == 0); + correct &= CheckFileBytes(filename, expected, mode, reopen); + } + } + Require(std::filesystem::remove(filename)); + Require(std::filesystem::remove(directory)); + return correct; +} + int main() { Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); @@ -214,4 +267,5 @@ int main() { Require(freopen_nid_postfix(filename.c_str(), "rb", &failed) == nullptr); Require(*__error_nid_postfix() == 2); Require(failed.GuestState().flags == 0 && failed.GuestState().descriptor == -1); + return CheckBinaryModes() ? 0 : 1; } From 73facbc3d1fbfc9350a08b198aefa5cf5c313d31 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 13:45:10 -0700 Subject: [PATCH 118/203] fix(libc): translate Windows fscanf guest arguments --- .../libs/prx/libc/include/WindowsScanning.hpp | 21 +++++++++++-- core/libs/prx/libc/src/Formatting.cpp | 2 +- core/libs/tests/GuestStandardStreams.cpp | 30 +++++++++++++++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index ff7701281..e4dd53c00 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -7,8 +7,9 @@ namespace LibcDetail { -inline int ScanWindows(const char* input, const char* format, const void* source) { - if (!input || !format || !source) { errno = 22; return EOF; } +template +inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* source, TScanner scan) { + if (!format || !source) { errno = 22; return EOF; } std::string translated; std::vector pointers; FormatArguments args(source); @@ -45,7 +46,21 @@ inline int ScanWindows(const char* input, const char* format, const void* source } if (!suppressed) pointers.push_back(args.Next()); } - return std::vsscanf(input, translated.c_str(), reinterpret_cast(pointers.data())); + return scan(translated.c_str(), reinterpret_cast(pointers.data())); +} + +inline int ScanWindows(const char* input, const char* format, const void* source) { + if (!input) { errno = 22; return EOF; } + return ScanWindowsArguments_nid_no_patch(format, source, [input](const char* translated, char* pointers) { + return std::vsscanf(input, translated, pointers); + }); +} + +inline int ScanFileWindows_nid_no_patch(std::FILE* stream, const char* format, const void* source) { + if (!stream) { errno = 22; return EOF; } + return ScanWindowsArguments_nid_no_patch(format, source, [stream](const char* translated, char* pointers) { + return std::vfscanf(stream, translated, pointers); + }); } } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0cc9b6e63..61cbc5c72 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -208,7 +208,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); - const int result = std::vfscanf(native, format, *reinterpret_cast(args)); + const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else std::va_list args; diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index ee322b0d4..44a43de61 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -168,15 +168,41 @@ int main() { FileStream scanned(std::tmpfile()); Require(fprintf_nid_postfix(&scanned, "%d %s", 42, "answer") == 9); std::rewind(scanned.GetHandle()); -#ifndef _WIN32 + Require(fscanf_nid_postfix(&scanned, "%*d") == 0); + Require(ftello_nid_postfix(&scanned) == 2); + std::rewind(scanned.GetHandle()); int scannedNumber = 0; char scannedWord[16]{}; + std::fputs("fscanf: suppressed conversion passed; assigning register arguments\n", stderr); + std::fflush(stderr); Require(fscanf_nid_postfix(&scanned, "%d %15s", &scannedNumber, scannedWord) == 2); Require(scannedNumber == 42 && std::strcmp(scannedWord, "answer") == 0); Require(fscanf_nid_postfix(&scanned, "%d", &scannedNumber) == EOF); -#endif + Require(feof_nid_postfix(&scanned) && (scanned.GuestState().flags & 0x20)); scanned.Close(); + FileStream scanMany(std::tmpfile()); + Require(std::fputs("7 1 2 3 4 5 6 7 8 4294967297 -4294967298 4294967299 abc %!", scanMany.GetHandle()) >= 0); + std::rewind(scanMany.GetHandle()); + int numbers[8]{}; + std::int64_t large = 0, negative = 0; + std::uint64_t sized = 0; + char letters[4]{}; + std::int64_t consumed = -1; + Require(fscanf_nid_postfix(&scanMany, "%*d %d %d %d %d %d %d %d %d %ld %jd %zu %3[a-z] %%%ln", + &numbers[0], &numbers[1], &numbers[2], &numbers[3], &numbers[4], &numbers[5], &numbers[6], &numbers[7], + &large, &negative, &sized, letters, &consumed) == 12); + for (int i = 0; i < 8; ++i) Require(numbers[i] == i + 1); + Require(large == INT64_C(4294967297) && negative == -INT64_C(4294967298) && sized == UINT64_C(4294967299)); + Require(std::strcmp(letters, "abc") == 0 && consumed == ftello_nid_postfix(&scanMany)); + Require(fgetc_nid_postfix(&scanMany) == '!'); + int unmatched = 123; + Require(fseeko_nid_postfix(&scanMany, -1, SEEK_CUR) == 0); + Require(fscanf_nid_postfix(&scanMany, "%d", &unmatched) == 0 && unmatched == 123); + Require(fgetc_nid_postfix(&scanMany) == '!'); + Require(fscanf_nid_postfix(&scanMany, "%d", &unmatched) == EOF && unmatched == 123); + scanMany.Close(); + FileStream positioned(std::tmpfile()); constexpr std::int64_t largeOffset = INT64_C(4294967313); Require(fseeko_nid_postfix(&positioned, largeOffset, SEEK_SET) == 0); From 984bc57eca0b17564bc3da7c16c638f0ff817217 Mon Sep 17 00:00:00 2001 From: DotDebian Date: Fri, 2 Oct 2026 19:01:36 +0200 Subject: [PATCH 119/203] feat(shader): implement ds_wrxchg2_rtn_b32 and ds_wrxchg2st64_rtn_b32 Both swap data0 with the dword at addr + offset0 and data1 with the dword at addr + offset1, and return the two previous values in vdst and vdst + 1. The offsets are scaled by 4 bytes, or by 256 bytes for the st64 form. Each location goes through the existing SharedAtomicSwap32. An encoding whose two offsets are equal is rejected at decode time. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/DsWrxchg2.cpp | 158 ++++++++++++++++++ .../include/RdnaDecoder/RdnaOpcode.hpp | 2 + .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 16 +- .../Translation/TranslationContext.hpp | 1 + .../MemoryInstructionDispatch.cpp | 3 + .../SharedMemoryInstructions.cpp | 18 ++ 6 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp new file mode 100644 index 000000000..c1ee2ba6a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp @@ -0,0 +1,158 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t ActiveLanes = 16; +constexpr std::uint32_t LdsDwords = 256; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr auto Code = std::to_array({ + 0x34020082, 0x34060084, 0x343c0082, + 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, 0xe030200c, 0x80000701, + 0xbf8c3f70, + 0xd8340000, 0x0000041e, 0xd8340080, 0x0000051e, 0xd8340100, 0x0000051e, 0xd8340300, 0x0000041e, + 0xd8340180, 0x0000051e, 0xd8340280, 0x0000041e, 0xd8340380, 0x0000041e, 0xd8340200, 0x0000051e, + 0xbf8cc07f, + 0x7e1c0306, 0x7e1e0307, 0x7e200306, 0x7e220307, + 0xd8b82000, 0x0a07061e, + 0xd8bc0103, 0x0c06071e, + 0xd8b8a060, 0x0e0f0e1e, + 0xbefe03ff, 0x0000ffff, + 0xd8b880e0, 0x1007061e, + 0xbefe03c1, + 0xbf8cc07f, + 0xd8d80000, 0x1400001e, 0xd8d80080, 0x1500001e, 0xd8d80300, 0x1600001e, 0xd8d80100, 0x1700001e, + 0xd8d80180, 0x1800001e, 0xd8d80280, 0x1900001e, 0xd8d80380, 0x1a00001e, 0xd8d80200, 0x1b00001e, + 0xbf8cc07f, + 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, + 0xe0702010, 0x80010e03, 0xe0702014, 0x80010f03, 0xe0702018, 0x80011003, 0xe070201c, 0x80011103, + 0xe0702020, 0x80011403, 0xe0702024, 0x80011503, 0xe0702028, 0x80011603, 0xe070202c, 0x80011703, + 0xe0702030, 0x80011803, 0xe0702034, 0x80011903, 0xe0702038, 0x80011a03, 0xe070203c, 0x80011b03, + 0xbf810000, +}); + +alignas(256) constexpr auto CoincidingOffsets = std::to_array({0xd8b80505, 0x0a07061e, 0xbf810000}); +alignas(256) constexpr auto CoincidingOffsetsSt64 = std::to_array({0xd8bc0000, 0x0a07061e, 0xbf810000}); +alignas(256) constexpr auto DestinationOverflow = std::to_array({0xd8b82000, 0xff07061e, 0xbf810000}); + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x01016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +template +void Translate(AgcDriver::VulkanDevice& device, std::span code, TUse&& use) { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 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}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + use(ShaderRecompiler::Recompile(request)); +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t word = 0; word < Inputs; ++word) { + Input[tid * Inputs + word] = ((word + 1u) << 28u) | (tid * 0x00010203u + word); + } + } + Output.fill(0xdeadbeefu); + Translate(device, Code, [&](const auto& result) { + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + }); +} + +void Reject(AgcDriver::VulkanDevice& device, std::span code, const std::string& reason) { + std::string error; + try { + Translate(device, code, [](const auto&) {}); + } catch (const std::exception& exception) { + error = exception.what(); + } + Require(error.find(reason) != std::string::npos, "ds wrxchg2: expected a rejection for \"" + reason + "\", got \"" + error + "\""); +} + +void CheckRejections(AgcDriver::VulkanDevice& device) { + Reject(device, CoincidingOffsets, "DS write exchange of one location through both offsets is not supported"); + Reject(device, CoincidingOffsetsSt64, "DS write exchange of one location through both offsets is not supported"); + Reject(device, DestinationOverflow, "DS write exchange destination register range overflow"); +} + +void Check() { + constexpr std::array names{ + "ds_wrxchg2_rtn_b32 offset0 result", "ds_wrxchg2_rtn_b32 offset1 result", + "ds_wrxchg2st64_rtn_b32 offset0 result", "ds_wrxchg2st64_rtn_b32 offset1 result", + "aliased ds_wrxchg2_rtn_b32 offset0 result", "aliased ds_wrxchg2_rtn_b32 offset1 result", + "masked ds_wrxchg2_rtn_b32 offset0 result", "masked ds_wrxchg2_rtn_b32 offset1 result", + "ds_wrxchg2_rtn_b32 offset0 memory", "ds_wrxchg2_rtn_b32 offset1 memory", + "ds_wrxchg2st64_rtn_b32 offset0 memory", "ds_wrxchg2st64_rtn_b32 offset1 memory", + "aliased ds_wrxchg2_rtn_b32 offset0 memory", "aliased ds_wrxchg2_rtn_b32 offset1 memory", + "masked ds_wrxchg2_rtn_b32 offset0 memory", "masked ds_wrxchg2_rtn_b32 offset1 memory", + }; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t a = Input[tid * Inputs]; + const std::uint32_t b = Input[tid * Inputs + 1]; + const std::uint32_t c = Input[tid * Inputs + 2]; + const std::uint32_t d = Input[tid * Inputs + 3]; + const bool active = tid < ActiveLanes; + const std::array expected{ + a, b, a, b, b, a, active ? a : c, active ? b : d, + c, d, d, c, c, d, active ? c : a, active ? d : b, + }; + for (std::uint32_t j = 0; j < Results; ++j) { + const auto actual = Output[tid * Results + j]; + Require(actual == expected[j], std::string("ds wrxchg2: lane ") + std::to_string(tid) + " " + names[j] + " is " + Hex(actual) + ", expected " + Hex(expected[j])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + CheckRejections(*device); + std::puts("ds wrxchg2 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index ef4daa6c7..0a267265f 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -858,6 +858,8 @@ enum class RdnaOpcode : std::uint16_t { DsXorB32, DsXorRtnB32, DsWrxchgRtnB32, + DsWrxchg2RtnB32, + DsWrxchg2st64RtnB32, DsMinF32, DsMaxF32, DsSwizzleB32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index a1a6484f4..f8c180d05 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -304,6 +304,8 @@ constexpr MemoryOpcodeInfo dsOpcodes[] = { {0x2au, RdnaOpcode::DsOrRtnB32, 1, 32, false, false, false}, {0x2bu, RdnaOpcode::DsXorRtnB32, 1, 32, false, false, false}, {0x2du, RdnaOpcode::DsWrxchgRtnB32, 1, 32, false, false, false}, + {0x2eu, RdnaOpcode::DsWrxchg2RtnB32, 2, 32, false, false, false}, + {0x2fu, RdnaOpcode::DsWrxchg2st64RtnB32, 2, 32, false, false, false}, {0x35u, RdnaOpcode::DsSwizzleB32, 1, 32, false, false, false}, {0x36u, RdnaOpcode::DsReadB32, 1, 32, false, false, false}, {0x37u, RdnaOpcode::DsRead2B32, 2, 32, false, false, false}, @@ -531,6 +533,8 @@ bool isDsAtomicOpcode(RdnaOpcode opcode) { std::uint32_t dsSourceCount(RdnaOpcode opcode) { switch (opcode) { + case RdnaOpcode::DsWrxchg2RtnB32: + case RdnaOpcode::DsWrxchg2st64RtnB32: case RdnaOpcode::DsWrite2B32: case RdnaOpcode::DsWrite2st64B32: case RdnaOpcode::DsWrite2B64: @@ -899,6 +903,14 @@ RdnaInstruction DecodeRdnaDs(std::uint32_t programCounter, std::span Date: Fri, 2 Oct 2026 19:11:03 +0200 Subject: [PATCH 120/203] test(shader): mask the ds_wrxchg2 lanes with a per-lane compare The masked case loaded EXEC from a scalar literal. A scalar mask is read by subgroup lane, so on a host whose subgroups are narrower than the wave (llvmpipe: 8 lanes) lane 16 took bit 0 and stayed active. v_cmpx_gt_u32 on the thread id gives the same low 16 lanes on any subgroup size. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp index c1ee2ba6a..9da6c4caf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp @@ -35,7 +35,7 @@ alignas(256) constexpr auto Code = std::to_array({ 0xd8b82000, 0x0a07061e, 0xd8bc0103, 0x0c06071e, 0xd8b8a060, 0x0e0f0e1e, - 0xbefe03ff, 0x0000ffff, + 0x7da80090, 0xd8b880e0, 0x1007061e, 0xbefe03c1, 0xbf8cc07f, From 12008dc2ba01774808f2c479bbe068818cb32a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 22:46:40 +0200 Subject: [PATCH 121/203] docs(technical-debt): note the ds_wrxchg2 per-lane exchange order gap --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..34022a21f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -139,6 +139,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. +- [`ds_wrxchg2_rtn_b32` and `ds_wrxchg2st64_rtn_b32`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). From 159d06d27ae51379495ab5bfe6bc66e822d582aa Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 18:05:46 -0300 Subject: [PATCH 122/203] feat(libSceAgcDriver): execute COND_WRITE COND_WRITE (opcode 0x45) with a memory poll and a memory destination is validated and executed: the driver drains the queue like WRITE_DATA, reads the poll dword through the range synchronization, compares it masked with the reference like a 32-bit WAIT_REG_MEM, and writes the data dword on a match. Register polls and register or scratch destinations are rejected. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Pm4Opcodes.hpp | 1 + .../src/Driver/Packets/PacketMemory.cpp | 2 +- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 22 +++++- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 79 ++++++++++++++++++- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp index b5522dda3..e78777f28 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp @@ -41,6 +41,7 @@ inline constexpr Opcode Opcodes[] = { {0x41, "CP_DMA"}, {0x42, "PFP_SYNC_ME"}, {0x43, "SURFACE_SYNC"}, + {0x45, "COND_WRITE"}, {0x46, "EVENT_WRITE"}, {0x47, "EVENT_WRITE_EOP"}, {0x48, "EVENT_WRITE_EOS"}, diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index f77bb313a..442cae836 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -163,7 +163,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) - : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); + : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); if (drains) { static const bool unlockedDrain = std::getenv("APS5_NO_UNLOCKED_DRAIN") == nullptr && !drainAll; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 26f818d92..fb1d124a8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -167,7 +167,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { } switch (opcode) { case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27: - case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x46: case 0x50: + case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x45: case 0x46: case 0x50: case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: case 0x83: case 0x9f: return {}; case 0x24: case 0x25: case 0x2c: case 0x38: @@ -382,6 +382,15 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[2] & (opcode == 0x3c ? 3u : 7u)) == 0, "misaligned WAIT_REG_MEM address"); break; } + case 0x45: { + size(9); + require((packet[1] & ~0x317u) == 0, "COND_WRITE reserved fields are not implemented"); + require((packet[1] & 0x10u) != 0, "register-space COND_WRITE poll is not implemented"); + require((packet[1] & 7u) <= 6u, "invalid COND_WRITE compare function"); + require(((packet[1] >> 8u) & 3u) == 1u, "register or scratch COND_WRITE destination is not implemented"); + require((packet[2] & 3u) == 0 && (packet[6] & 3u) == 0, "misaligned COND_WRITE address"); + break; + } case 0x49: { size(8); const auto dataSelect = packet[2] >> 29u; @@ -579,7 +588,7 @@ bool UsesGpuCacheBarrier(std::span packet) { bool AccessesMemory(std::uint32_t header) { switch ((header >> 8u) & 0xffu) { - case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; + case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; default: return false; } } @@ -808,6 +817,15 @@ void Execute(std::span packet, QueueState& queue) { case 0x83: GuestMemory::Write(address(packet[3], packet[4]), std::as_bytes(std::span(queue.constantRam).subspan(packet[1] / 4, packet[2])), 4); return; + case 0x45: { + std::uint32_t value = 0; + { + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait); + GuestMemory::Read(address(packet[2], packet[3]), std::as_writable_bytes(std::span(&value, 1)), 4); + } + if (waitCompares(packet, false, value)) GuestMemory::Write(address(packet[6], packet[7]), std::as_bytes(packet.subspan(8, 1)), 4); + return; + } case 0x37: { const auto destination = address(packet[2], packet[3]); if ((packet[1] & 0x10000u) != 0) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 3af51bbd8..ebaa9c657 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -59,7 +59,7 @@ void testCatalog() { const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]); if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str()); } - check(values.size() == 54, "reference opcode catalog is incomplete"); + check(values.size() == 55, "reference opcode catalog is incomplete"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known"); const std::array, 11> custom{{ {5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"}, @@ -780,6 +780,81 @@ void testPredicatedSubmission() { AgcDriverWaitIdle_nid_postfix(); } +std::vector condWrite(std::uint32_t control, const std::uint32_t& poll, std::uint32_t reference, std::uint32_t mask, std::uint32_t& target, std::uint32_t value) { + return makePacket(0x45, {control, low(&poll), high(&poll), reference, mask, low(&target), high(&target), value}); +} + +void testConditionalWrite() { + AgcDriver::QueueState state; + alignas(4) std::uint32_t poll = 0x1234u; + alignas(4) std::uint32_t target = 0; + const auto valid = condWrite(0x113, poll, 0x34, 0xff, target, 7); + check(AgcDriver::Pm4::Name(valid[0]) == "COND_WRITE" && valid[0] == 0xc0074500u, "COND_WRITE header or name mismatch"); + check(AgcDriver::Pm4::UnsupportedReason(valid[0]).empty() && AgcDriver::Pm4::AccessesMemory(valid[0]), "COND_WRITE is rejected or does not synchronize guest memory"); + + const std::array, 7> functions{{{0, true}, {1, false}, {2, true}, {3, true}, {4, false}, {5, true}, {6, false}}}; + for (const auto& [function, writes] : functions) { + target = 0; + execute(state, condWrite(0x110 | function, poll, 0x34, 0xff, target, 9)); + check(target == (writes ? 9u : 0u), "COND_WRITE compared the masked poll value incorrectly"); + } + target = 0; + execute(state, condWrite(0x111, poll, 0x35, 0xff, target, 10)); + check(target == 10, "COND_WRITE less-than did not write"); + target = 0; + execute(state, condWrite(0x116, poll, 0x1233, 0xffffffffu, target, 11)); + check(target == 11, "COND_WRITE greater-than with a full mask did not write"); + + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x45, {0x113, low(&poll), high(&poll), 0, 0, low(&target), high(&target)}), 0); }, "packet size"); + expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x2000113, poll, 0, 0, target, 0), 0); }, "reserved"); + expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x103, poll, 0, 0, target, 0), 0); }, "register-space"); + expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x117, poll, 0, 0, target, 0), 0); }, "compare function"); + expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x013, poll, 0, 0, target, 0), 0); }, "destination"); + expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x213, poll, 0, 0, target, 0), 0); }, "destination"); + auto misaligned = condWrite(0x113, poll, 0, 0, target, 0); + misaligned[2] += 2; + expectFailure([&] { AgcDriver::Pm4::Validate(misaligned, 0); }, "misaligned"); + misaligned = condWrite(0x113, poll, 0, 0, target, 0); + misaligned[6] += 1; + expectFailure([&] { AgcDriver::Pm4::Validate(misaligned, 0); }, "misaligned"); + + struct MemoryState { + std::uint32_t poll = 0; + std::uint32_t target = 0; + bool read = false; + bool written = false; + }; + static MemoryState memory; + memory = {}; + struct FlushHookReset { + ~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } reset; + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + check(bytes == sizeof(std::uint32_t), "COND_WRITE resolved an unrelated range"); + if (address == reinterpret_cast(&memory.target)) { + memory.written = true; + } else { + check(address == reinterpret_cast(&memory.poll), "COND_WRITE resolved an unrelated poll address"); + memory.poll = 5; + memory.read = true; + } + }); + execute(state, condWrite(0x113, memory.poll, 5, 0xffffffffu, memory.target, 12)); + check(memory.read && memory.written && memory.target == 12, "COND_WRITE used a stale poll value or skipped synchronizing its target"); +} + +void testConditionalWriteSubmission() { + alignas(4) static std::uint32_t poll = 0; + static std::array results{}; + results.fill(0); + poll = 0; + auto words = joinPackets({writeWord(poll, 5), condWrite(0x113, poll, 5, 0xffffffffu, results[0], 51), condWrite(0x114, poll, 5, 0xffffffffu, results[1], 52)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + check(results[0] == 51, "COND_WRITE read its poll value before an earlier packet of its queue stored it"); + check(results[1] == 0, "COND_WRITE wrote although its comparison failed"); +} + void testAsyncMemoryFailure() { auto commands = makePacket(0x37, {0x100, 0x1000, 0, 1}); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; @@ -815,6 +890,8 @@ int main(int argc, char** argv) { testConditionReadSynchronization(); testEventWrite(); testAcquireMem(); + testConditionalWrite(); + testConditionalWriteSubmission(); testPredication(); testDriverSubmission(); testPredicatedSubmission(); From d8b775ec19750c5a35373dbd87fd34c453ec9968 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 17:57:47 -0300 Subject: [PATCH 123/203] feat(libSceAgc): implement sceAgcCbCondWrite sceAgcCbCondWrite writes the 9-dword COND_WRITE packet: poll a memory dword, compare it masked with the reference, and write a dword to memory on a match. sceAgcCbCondWriteGetSize returns its 36 bytes. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/CMakeLists.txt | 19 ++++ core/libs/prx/libSceAgc/Cb/src/Branch.cpp | 21 ++--- core/libs/prx/libSceAgc/tests/CbCondWrite.cpp | 86 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/CbCondWrite.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index d3bb48f0b..a91e2dd88 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -124,6 +124,25 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_cb_cond_write_tests EXCLUDE_FROM_ALL + tests/CbCondWrite.cpp + Command/src/Packet.cpp + Command/src/Memory.cpp + Command/src/Control.cpp + Cb/src/Branch.cpp +) +target_include_directories(agc_cb_cond_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_cb_cond_write_tests PRIVATE libc) +configure_windows_unwind(agc_cb_cond_write_tests) +if(BUILD_TESTING) + set_target_properties(agc_cb_cond_write_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_cb_cond_write_tests COMMAND agc_cb_cond_write_tests) + if(WIN32) + set_tests_properties(agc_cb_cond_write_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_payload_range_tests EXCLUDE_FROM_ALL tests/PayloadRange.cpp) target_include_directories(agc_payload_range_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_payload_range_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceAgc/Cb/src/Branch.cpp b/core/libs/prx/libSceAgc/Cb/src/Branch.cpp index 649b810f6..d27f1c28c 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Branch.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Branch.cpp @@ -28,19 +28,20 @@ std::uint32_t APS5_VABI sceAgcCbBranchGetSize() { return 56; } -uint32_t* APS5_VABI sceAgcCbCondWrite(CommandBuffer* buf, uint32_t cond, uint32_t a, uint32_t b, uint32_t c) { - (void)buf; - (void)cond; - (void)a; - (void)b; - (void)c; - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcCbCondWrite(CommandBuffer* buf, std::uint32_t compareFunction, std::uint32_t writeSpace, const volatile void* writeAddress, std::uint32_t writeData, const volatile void* pollAddress, std::uint32_t reference, std::uint32_t mask) { + Agc::Command::Require(compareFunction <= 6, __func__, "invalid write comparison"); + Agc::Command::Require(writeSpace == 1, __func__, "only memory writes are supported"); + const auto destination = reinterpret_cast(writeAddress); + const auto source = reinterpret_cast(pollAddress); + Agc::Command::CheckAddress(destination, 4, __func__); + Agc::Command::CheckAddress(source, 4, __func__); + Agc::Command::CheckBits(destination, 0xffffffffffffull, __func__); + Agc::Command::CheckBits(source, 0xffffffffffffull, __func__); + return Agc::Command::Emit(buf, 0x45u, {0x10u | compareFunction | (writeSpace << 8u), static_cast(source), static_cast(source >> 32u), reference, mask, static_cast(destination), static_cast(destination >> 32u), writeData}, __func__); } std::uint32_t APS5_VABI sceAgcCbCondWriteGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 36; } } diff --git a/core/libs/prx/libSceAgc/tests/CbCondWrite.cpp b/core/libs/prx/libSceAgc/tests/CbCondWrite.cpp new file mode 100644 index 000000000..69186e10b --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/CbCondWrite.cpp @@ -0,0 +1,86 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcCbCondWrite(CommandBuffer*, std::uint32_t, std::uint32_t, const volatile void*, std::uint32_t, const volatile void*, std::uint32_t, std::uint32_t); +std::uint32_t APS5_VABI sceAgcCbCondWriteGetSize(); +} + +namespace { + +constexpr std::uint32_t Sentinel = 0xabcdef01u; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action) { + try { + action(); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error("expected invalid input to fail"); +} + +const volatile void* at(std::uintptr_t address) { + return reinterpret_cast(address); +} + +struct Storage { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + + Storage() { words.fill(Sentinel); } +}; + +void testPacket() { + for (std::uint32_t compareFunction = 0; compareFunction <= 6; ++compareFunction) { + Storage storage; + const auto* packet = sceAgcCbCondWrite(&storage.buffer, compareFunction, 1, at(0x0000123456789abcu), 0xdeadbeefu, at(0x0000fedcba987654u), 0x11223344u, 0xff00ff00u); + check(packet == storage.words.data() && packet[0] == 0xc0074500u, "COND_WRITE header mismatch"); + check(packet[1] == (0x110u | compareFunction), "COND_WRITE control mismatch"); + check(packet[2] == 0xba987654u && packet[3] == 0x0000fedcu, "COND_WRITE poll address mismatch"); + check(packet[4] == 0x11223344u && packet[5] == 0xff00ff00u, "COND_WRITE reference or mask mismatch"); + check(packet[6] == 0x56789abcu && packet[7] == 0x00001234u, "COND_WRITE write address mismatch"); + check(packet[8] == 0xdeadbeefu, "COND_WRITE data mismatch"); + check(storage.buffer.cursor_up == storage.words.data() + 9 && storage.words[9] == Sentinel, "COND_WRITE cursor advance"); + } + check(sceAgcCbCondWriteGetSize() == 9 * sizeof(std::uint32_t), "COND_WRITE size mismatch"); +} + +void testInvalid() { + Storage storage; + const auto before = storage.words; + const auto poll = at(0x1000u); + const auto write = at(0x2000u); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 7, 1, write, 0, poll, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 0, write, 0, poll, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 2, write, 0, poll, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, nullptr, 0, poll, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, at(0x2002u), 0, poll, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, write, 0, nullptr, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, write, 0, at(0x1001u), 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, at(0x0001000000000000u), 0, poll, 0, 0); }); + expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, write, 0, at(0x0001000000000000u), 0, 0); }); + check(storage.words == before && storage.buffer.cursor_up == storage.words.data(), "failed COND_WRITE modified the buffer"); +} + +} + +int main() { + try { + testPacket(); + testInvalid(); + std::puts("AGC COND_WRITE tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1f4735015..939ce57c9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -52,6 +52,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws +- [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [qj7QZpgr9Uw](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name From 7e3dcbc27e4f7bbdc4942436c418b0864cf2b53a Mon Sep 17 00:00:00 2001 From: CnR Date: Tue, 6 Oct 2026 16:20:07 -0500 Subject: [PATCH 124/203] feat(libkernel): implement sceKernelReleaseFlexibleMemory (#952) * feat(libkernel): implement sceKernelReleaseFlexibleMemory Unmaps the range through sceKernelMunmap, which returns its pages to the flexible memory budget, as fpPS4 and prosper do. Co-Authored-By: Claude Opus 5.5 * test(libkernel): keep the flexible memory release check apart from the batch map checks Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../prx/libkernel/DirectMemory/Export.cpp | 4 ++++ core/libs/prx/libkernel/Unimplemented.cpp | 5 ----- core/libs/tests/GuestMemory.cpp | 19 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index da96d2008..a08440be7 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -207,6 +207,10 @@ int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { return result; } +int APS5_VABI sceKernelReleaseFlexibleMemory(void* addr, size_t len) { + return sceKernelMunmap(reinterpret_cast(addr), len); +} + int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { if (start < 0 || len == 0) return SCE_KERNEL_ERROR_EINVAL; DirectMemoryFree(start, len); diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 96466573a..18df01488 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -15,11 +15,6 @@ int APS5_VABI __tls_get_addr_nid_postfix(void) { return 0; } -int APS5_VABI sceKernelReleaseFlexibleMemory(void) { - NotImplemented_nid_no_patch("teiItL2boFw"); - return 0; -} - // Canonical lib is libScePosix (dead import of Cyberpunk 2077): 35 of its // 36 sibling imports resolve to libkernel, and libScePosix is not in the // game's NEEDED list so only a NEEDED module can satisfy the loader here. diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 27dd441ab..59b3fb0ac 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -41,6 +41,7 @@ int APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void**, std::size_t, int, int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); @@ -77,6 +78,23 @@ static const char* NameAt(const void* address) { return info.name; } +static void CheckReleaseFlexibleMemory() { + constexpr std::size_t length = 0x10000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 1; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelReleaseFlexibleMemory(mapped, length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + void* again = mapped; + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == 0 && again == mapped); + Require(static_cast(again)[length - 1] == 0); + Require(sceKernelMunmap(again, length) == 0); +} + static void CheckNamedAndHintedMappings() { constexpr std::size_t length = 0x10000; void* first = nullptr; @@ -830,6 +848,7 @@ static void CheckDirectMemoryWriteWatch() { #endif int main() { + CheckReleaseFlexibleMemory(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); CheckBatchMapStopsAtInvalidEntry(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8543e95d7..885bcbb8e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -67,6 +67,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature - [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. Not implemented on Windows +- [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written From 0653b8749757840dd5c6c43198be327c96e4adb9 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 13:57:08 -0700 Subject: [PATCH 125/203] fix(libc): reject overlapping memcpy_s ranges --- core/libs/prx/libc/src/Strings.cpp | 7 +++++++ core/libs/tests/GuestStrings.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 7c9be2a2e..8cba0d618 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -227,6 +227,13 @@ int APS5_VABI memcpy_s_nid_postfix(void* dest, size_t destsz, const void* src, s std::memset(dest, 0, destsz); return src ? GuestErange : GuestEinval; } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (count != 0 && distance < count) { + std::memset(dest, 0, destsz); + return GuestEinval; + } std::memcpy(dest, src, count); return 0; } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 808f69407..145980fed 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -34,6 +34,33 @@ static void Require(bool condition) { } } +static bool CheckMemcpyOverlap() { + bool correct = true; + for (const auto offset : {0, 1, -1, 3, -3}) { + unsigned char bytes[16]; + std::memset(bytes, 0x5a, sizeof(bytes)); + auto* destination = bytes + 4; + const auto* source = destination + offset; + const int error = memcpy_s_nid_postfix(destination, 8, source, 4); + bool cleared = true; + for (unsigned i = 4; i < 12; ++i) cleared &= bytes[i] == 0; + const bool matches = error == 22 && cleared && bytes[3] == 0x5a && bytes[12] == 0x5a; + if (!matches) std::fprintf(stderr, "memcpy_s overlap %+d: expected EINVAL 22 and eight zero bytes, received %d\n", offset, error); + correct &= matches; + } + unsigned char adjacent[] = {1, 2, 3, 4, 5, 6, 7, 8}; + Require(memcpy_s_nid_postfix(adjacent, 4, adjacent + 4, 4) == 0); + Require(std::memcmp(adjacent, adjacent + 4, 4) == 0); + const unsigned char original[] = {1, 2, 3, 4, 5, 6, 7, 8}; + std::memcpy(adjacent, original, sizeof(adjacent)); + Require(memcpy_s_nid_postfix(adjacent + 4, 4, adjacent, 4) == 0); + const unsigned char unchanged[] = {1, 2, 3, 4, 1, 2, 3, 4}; + Require(std::memcmp(adjacent, unchanged, sizeof(adjacent)) == 0); + Require(memcpy_s_nid_postfix(adjacent, sizeof(adjacent), adjacent, 0) == 0); + Require(std::memcmp(adjacent, unchanged, sizeof(adjacent)) == 0); + return correct; +} + static void CheckBoundsCheckedFunctions() { char small[4] = "zz"; Require(strcpy_s_nid_postfix(small, sizeof(small), "abc") == 0 && std::strcmp(small, "abc") == 0); @@ -131,4 +158,5 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + return CheckMemcpyOverlap() ? 0 : 1; } From 62de7ab8ac083946dcf305efc8863d4990ae3b32 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 14:28:56 -0700 Subject: [PATCH 126/203] fix(libc): keep wide printf precision on UTF-8 boundaries --- .../prx/libc/include/WindowsFormatting.hpp | 21 +++++++++++-- core/libs/tests/Formatting.cpp | 30 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 19d02b612..43c99d111 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -146,15 +146,25 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } else { while (*format >= '0' && *format <= '9') spec += *format++; } + size_t precisionLimit = std::numeric_limits::max(); if (*format == '.') { ++format; if (*format == '*') { ++format; const int precision = args.Next(); - if (precision >= 0) spec += "." + std::to_string(precision); + if (precision >= 0) { + spec += "." + std::to_string(precision); + precisionLimit = static_cast(precision); + } } else { spec += '.'; - while (*format >= '0' && *format <= '9') spec += *format++; + precisionLimit = 0; + while (*format >= '0' && *format <= '9') { + const auto digit = static_cast(*format - '0'); + precisionLimit = precisionLimit <= (std::numeric_limits::max() - digit) / 10 + ? precisionLimit * 10 + digit : std::numeric_limits::max(); + spec += *format++; + } } } std::string length; @@ -209,10 +219,15 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo const char16_t* value = args.Next(); if (!value) value = u"(null)"; std::string utf8; - while (*value) { + while (utf8.size() < precisionLimit && *value) { char32_t code = *value++; if (code >= 0xd800 && code < 0xdc00 && *value >= 0xdc00 && *value < 0xe000) code = 0x10000 + ((code - 0xd800) << 10) + (*value++ - 0xdc00); + const auto previousSize = utf8.size(); AppendUtf8(utf8, code); + if (utf8.size() > precisionLimit) { + utf8.resize(previousSize); + break; + } } output.Value(spec + 's', utf8.c_str()); } else if (conversion == 'p' && length.empty()) { diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 7550e68a9..12a89c00f 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -38,6 +38,34 @@ static int APS5_VABI PrintList(const char* format, ...) { return result; } +static bool CheckWidePrecision() { + const char16_t input[] = u"A\u00e9\u20ac\U0001f600Z"; + const char* expected[] = {"", "A", "A", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9\xe2\x82\xac", + "A\xc3\xa9\xe2\x82\xac", "A\xc3\xa9\xe2\x82\xac", "A\xc3\xa9\xe2\x82\xac", + "A\xc3\xa9\xe2\x82\xac\xf0\x9f\x98\x80", "A\xc3\xa9\xe2\x82\xac\xf0\x9f\x98\x80Z"}; + bool correct = true; + for (int precision = 0; precision < 12; ++precision) { + char output[32]; + std::memset(output, '!', sizeof(output)); + const int count = snprintf_nid_postfix(output, sizeof(output), "%.*ls", precision, input); + const auto expectedSize = std::strlen(expected[precision]); + const bool matches = count == expectedSize && std::strcmp(output, expected[precision]) == 0 && output[expectedSize + 1] == '!'; + if (!matches) std::fprintf(stderr, "wide precision %d: expected %zu complete UTF-8 bytes, received %d\n", precision, expectedSize, count); + correct &= matches; + } + if (!correct) return false; + char output[32]; + Require(snprintf_nid_postfix(output, sizeof(output), "[%5.1ls]", u"\u00e9") == 7 && std::strcmp(output, "[ ]") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "[%-5.2ls]", u"\u00e9") == 7 && std::strcmp(output, "[\xc3\xa9 ]") == 0); + Require(FormatList(output, sizeof(output), "%.3ls", u"\U0001f600") == 0 && output[0] == 0); + Require(FormatList(output, sizeof(output), "%.*ls:%d", -1, u"\u00e9", 7) == 4 && std::strcmp(output, "\xc3\xa9:7") == 0); + const char16_t bounded[] = {u'A', u'B'}; + Require(snprintf_nid_postfix(output, sizeof(output), "%.2ls", bounded) == 2 && std::strcmp(output, "AB") == 0); + Require(snprintf_nid_postfix(nullptr, 0, "%.1ls", u"\u00e9") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "%.1s", "\xc3\xa9") == 1 && static_cast(output[0]) == 0xc3 && output[1] == 0); + return correct; +} + __attribute__((noinline)) static void APS5_VABI RunChecks() { char buffer[1024]; Require(FormatList(buffer, sizeof(buffer), "Mount requested: %d", 0) == 18); @@ -85,4 +113,4 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { std::puts("Formatting checks passed: 10000 iterations"); } -int main() { RunChecks(); } +int main() { RunChecks(); return CheckWidePrecision() ? 0 : 1; } From 956a8b7d5fd8cfacc44948729173351a93804f43 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 18:29:36 -0300 Subject: [PATCH 127/203] fix(libSceVideoRecordingP): correct the sceVideoRecordingOpen signature sceVideoRecordingOpen takes the target path, the recording param, the heap and the heap size, not (buffer, size, param). It still throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoRecordingP/Export.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 9400810ba..7d1cb61a2 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -37,10 +37,11 @@ int APS5_VABI sceVideoRecordingGetStatus(void) { return 0; } -int APS5_VABI sceVideoRecordingOpen(void* buffer, size_t size, const void* param) { - (void)buffer; - (void)size; +int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* heap, int heapSize) { + (void)path; (void)param; + (void)heap; + (void)heapSize; NotImplemented_nid_no_patch(__func__); return 0; } From 55cbd2d6f1f2558ae8bd084e057d0e5816391b3b Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:36:19 +0200 Subject: [PATCH 128/203] feat(userservice): implement foreground user, registered list, user colour and NP account id Four exports that titles call during start-up were missing. They now follow the single-user / offline model already used in this file (sceUserServiceGetInitialUser, sceUserServiceGetLoginUserIdList, sceUserServiceGetPlatformPrivacyWs1): - sceUserServiceGetForegroundUser(int*) -> the initial user - sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList*) -> initial user in slot 0, the other 15 slots invalid - sceUserServiceGetUserColor(int, int*) -> blue (0) - sceUserServiceGetNpAccountId(int, uint64_t*) -> 0 (no PSN account linked) Adds UserServiceRegisteredUserIdList (int[16]) to SceTypes.hpp and a CMakeLists.txt so the library can carry its guest test. Signatures follow the public OpenOrbis SDK headers. New test core/libs/tests/GuestUserService.cpp checks the valid returns, the pointer and wrong-user-id error paths; it fails without the change (the symbols are undefined) and passes with it. --- core/libs/SceTypes.hpp | 2 + .../libs/prx/libSceUserService/CMakeLists.txt | 20 +++++++++ core/libs/prx/libSceUserService/Export.cpp | 35 +++++++++++++++ core/libs/tests/GuestUserService.cpp | 44 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 core/libs/prx/libSceUserService/CMakeLists.txt create mode 100644 core/libs/tests/GuestUserService.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 84a48a94f..5bd1b5553 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1729,6 +1729,8 @@ struct SystemGestureTouchEvent { std::uint8_t reserve[168]; }; struct UserServiceLoginUserIdList { int user_id[4]; }; +struct UserServiceRegisteredUserIdList { int user_id[16]; }; + struct SceUserServiceEvent { std::uint32_t event_type; int user_id; diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt new file mode 100644 index 000000000..8eafc99ab --- /dev/null +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceUserService SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceUserService PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceUserService) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserService.cpp) + target_include_directories(guest_user_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service COMMAND guest_user_service_tests) + if(WIN32) + set_tests_properties(guest_user_service PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 9ae783818..2a54f7c9f 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -156,4 +156,39 @@ int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* valu return 0; } +int APS5_VABI sceUserServiceGetForegroundUser(int* user_id) { + if (user_id == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *user_id = USER_SERVICE_INITIAL_USER_ID; + return USER_SERVICE_OK; +} + +int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list) { + if (user_id_list == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + user_id_list->user_id[0] = USER_SERVICE_INITIAL_USER_ID; + for (int i = 1; i < 16; ++i) { + user_id_list->user_id[i] = USER_SERVICE_USER_ID_INVALID; + } + return USER_SERVICE_OK; +} + +int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) { + if (color == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *color = 0; + return USER_SERVICE_OK; +} + +int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) { + if (account_id == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *account_id = 0; + return USER_SERVICE_OK; +} + } diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp new file mode 100644 index 000000000..823c0536a --- /dev/null +++ b/core/libs/tests/GuestUserService.cpp @@ -0,0 +1,44 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); +int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); +int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); +int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + Require(sceUserServiceGetForegroundUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + int foreground = -1; + Require(sceUserServiceGetForegroundUser(&foreground) == USER_SERVICE_OK); + Require(foreground == USER_SERVICE_INITIAL_USER_ID); + + Require(sceUserServiceGetRegisteredUserIdList(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + UserServiceRegisteredUserIdList registered{}; + Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); + Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + for (int i = 1; i < 16; ++i) { + Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); + } + + int color = -1; + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(123, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK); + Require(color == 0); + + std::uint64_t account_id = 0xdeadbeefu; + Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetNpAccountId(123, &account_id) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, &account_id) == USER_SERVICE_OK); + Require(account_id == 0); +} From f467f5174f6a93e707d52f1f0a807ba709d0b73c Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:32:43 +0200 Subject: [PATCH 129/203] docs(userservice): record the assumed NP account id and default colour Per review: sceUserServiceGetNpAccountId reports success with account id 0 and sceUserServiceGetUserColor returns blue by default; both are assumptions. Records them under "Unknown function info" in docs/dev/TechnicalDebt.md. --- docs/dev/TechnicalDebt.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5e4153ff5..1ac04b12f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -51,6 +51,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown +- [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` +- [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws From fae2c8e6beda0d81cd4a6c7fc2aa46262dfff361 Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:41:38 +0200 Subject: [PATCH 130/203] feat(logindialog): implement the libSceLoginDialog state machine The seven libSceLoginDialog exports were throwing stubs. They now implement the common-dialog state machine (NONE -> INITIALIZED -> RUNNING -> FINISHED), mirroring the existing libSceSigninDialog: the local user is always signed in, so the dialog finishes as soon as it opens and a title waiting on it never blocks. - sceLoginDialogInitialize -> INITIALIZED, or 0x80B80004 if already initialized - sceLoginDialogOpen -> validates the state and the parameter, then FINISHED - sceLoginDialogUpdateStatus / sceLoginDialogGetStatus -> the status - sceLoginDialogGetResult -> zeroes the result (signed in) once FINISHED - sceLoginDialogClose -> ok, or 0x80B80003 when not initialized - sceLoginDialogTerminate -> NONE, or 0x80B80003 when not initialized The status values and the shared common-dialog error codes match libSceSigninDialog and libSceWebBrowserDialog. Adds a CMakeLists.txt so the library can carry its guest test. New test core/libs/tests/GuestLoginDialog.cpp drives the whole machine (the not-initialized and already-initialized errors, the argument and not-finished errors, open/get-result/close and terminate). It fails on main (the stubs throw) and passes with the change. --- .../libs/prx/libSceLoginDialog/CMakeLists.txt | 20 +++++ core/libs/prx/libSceLoginDialog/Export.cpp | 75 ++++++++++++------- core/libs/tests/GuestLoginDialog.cpp | 52 +++++++++++++ 3 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceLoginDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestLoginDialog.cpp diff --git a/core/libs/prx/libSceLoginDialog/CMakeLists.txt b/core/libs/prx/libSceLoginDialog/CMakeLists.txt new file mode 100644 index 000000000..0271b47ba --- /dev/null +++ b/core/libs/prx/libSceLoginDialog/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceLoginDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLoginDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLoginDialog) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_login_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestLoginDialog.cpp) + target_include_directories(guest_login_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_login_dialog_tests PRIVATE libSceLoginDialog) + add_test(NAME guest_login_dialog COMMAND guest_login_dialog_tests) + if(WIN32) + set_tests_properties(guest_login_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index 66c3b8374..f43475385 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -1,44 +1,69 @@ +#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 std::size_t LOGIN_DIALOG_RESULT_SIZE = 8; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceLoginDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceLoginDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceLoginDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceLoginDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); + 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 sceLoginDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceLoginDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); + 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 sceLoginDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); + return g_status.load(); +} + +int APS5_VABI sceLoginDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceLoginDialogGetResult(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; + std::memset(result, 0, LOGIN_DIALOG_RESULT_SIZE); + return 0; +} + +int APS5_VABI sceLoginDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceLoginDialogTerminate(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/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp new file mode 100644 index 000000000..01983f026 --- /dev/null +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -0,0 +1,52 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceLoginDialogInitialize(void); +int APS5_VABI sceLoginDialogOpen(const void* param); +int APS5_VABI sceLoginDialogUpdateStatus(void); +int APS5_VABI sceLoginDialogGetStatus(void); +int APS5_VABI sceLoginDialogGetResult(void* result); +int APS5_VABI sceLoginDialogClose(void); +int APS5_VABI sceLoginDialogTerminate(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); + +} + +int main() { + std::int64_t param = 0; + std::int64_t result = -1; + + Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); + + Require(sceLoginDialogInitialize() == 0); + Require(sceLoginDialogInitialize() == kErrAlreadyInitialized); + + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == kErrNotFinished); + + Require(sceLoginDialogOpen(¶m) == 0); + Require(sceLoginDialogGetStatus() == kStatusFinished); + Require(sceLoginDialogUpdateStatus() == kStatusFinished); + + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == 0); + Require(result == 0); + + Require(sceLoginDialogClose() == 0); + Require(sceLoginDialogTerminate() == 0); + Require(sceLoginDialogTerminate() == kErrNotInitialized); +} From 0178b6530e9db79078d530742b779d57ef2b2a03 Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:31:15 +0200 Subject: [PATCH 131/203] fix(logindialog): report user-cancelled and write only the leading result int32 sceLoginDialogGetResult zeroed 8 bytes of a result struct of unknown size and reported result 0 with user id 0. It now writes only the leading int32 (USER_CANCELED), like sceWebBrowserDialogGetResult and like the sign-in dialog, so a title takes its "no user chosen" path instead of reading an invalid user id. Records the dialog in the silent-stubs list in docs/dev/TechnicalDebt.md. --- core/libs/prx/libSceLoginDialog/Export.cpp | 6 +++--- core/libs/tests/GuestLoginDialog.cpp | 12 +++++++----- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index f43475385..a986655ab 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -16,8 +16,7 @@ constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80 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 std::size_t LOGIN_DIALOG_RESULT_SIZE = 8; +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -53,7 +52,8 @@ int APS5_VABI sceLoginDialogGetResult(void* result) { 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; - std::memset(result, 0, LOGIN_DIALOG_RESULT_SIZE); + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); return 0; } diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 01983f026..7eaeb5619 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -21,30 +21,32 @@ 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::int64_t result = -1; + std::int32_t result[2] = {-1, -1}; Require(sceLoginDialogTerminate() == kErrNotInitialized); Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); - Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); + Require(sceLoginDialogGetResult(result) == kErrNotInitialized); Require(sceLoginDialogInitialize() == 0); Require(sceLoginDialogInitialize() == kErrAlreadyInitialized); Require(sceLoginDialogOpen(nullptr) == kErrArgNull); - Require(sceLoginDialogGetResult(&result) == kErrNotFinished); + Require(sceLoginDialogGetResult(result) == kErrNotFinished); Require(sceLoginDialogOpen(¶m) == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); Require(sceLoginDialogUpdateStatus() == kStatusFinished); Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); - Require(sceLoginDialogGetResult(&result) == 0); - Require(result == 0); + Require(sceLoginDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5e4153ff5..4a8510769 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -15,6 +15,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceMsgDialog.native](../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog - [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) - [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged +- [libSceLoginDialog](../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing From 7cbf45b660e31b876bab4562323261c23ea844fb Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 16:30:24 -0500 Subject: [PATCH 132/203] feat(libs): declare the exports PPSA23566 imports that no library provides Co-Authored-By: Claude Opus 5.5 --- .../libSceAudioOut/src/AudioOut2System.cpp | 6 +++ core/libs/prx/libSceConvertKeycode/Export.cpp | 7 +++ core/libs/prx/libSceHttp/Export.cpp | 25 +++++++++ core/libs/prx/libSceJson2/Export.cpp | 12 +++++ core/libs/prx/libSceNet/Export.cpp | 6 +++ core/libs/prx/libSceNpCommerce/Export.cpp | 6 +++ .../prx/libSceNpEntitlementAccess/Export.cpp | 6 +++ core/libs/prx/libSceNpPartner001/Export.cpp | 17 ++++++ .../Export.cpp | 29 ++++++++++ core/libs/prx/libScePsml_debug/Export.cpp | 54 +++++++++++++++++++ core/libs/prx/libSceVoice/Export.cpp | 24 +++++++++ .../prx/libkernel/Pthread/Posix/Thread.cpp | 6 +++ docs/dev/TechnicalDebt.md | 12 +++++ 13 files changed, 210 insertions(+) create mode 100644 core/libs/prx/libSceNpPartner001/Export.cpp create mode 100644 core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index cc3fdc2ea..754cb5382 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -29,4 +29,10 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar return 0; } +APS5_EXPORT("iE8trxPKnAg", sceAudioOut2Unknown_iE8trxPKnAg); +int APS5_VABI sceAudioOut2Unknown_iE8trxPKnAg(void) { + NotImplemented_nid_no_patch("iE8trxPKnAg"); + return 0; +} + } diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index b643fcbb6..cc99aa240 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -21,4 +21,11 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint64_t a0, std::uint64_t static_cast(a0), static_cast(a1), static_cast(a2), static_cast(a3)); return kErrInvalidAddress; } + +APS5_EXPORT("S-Gni2KIJRY", sceConvertKeycodeUnknown_S_MGni2KIJRY); +int APS5_VABI sceConvertKeycodeUnknown_S_MGni2KIJRY(void) { + NotImplemented_nid_no_patch("S-Gni2KIJRY"); + return 0; +} + } diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 1d10d5348..673332c18 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -369,4 +369,29 @@ int APS5_VABI sceHttpGetLastErrno(int request_id, int* errno_out) { *errno_out = 0; return 0; } + +APS5_EXPORT("JBN6N-EY+3M", sceHttpUnknown_JBN6N_MEY_P3M); +int APS5_VABI sceHttpUnknown_JBN6N_MEY_P3M(void) { + NotImplemented_nid_no_patch("JBN6N-EY+3M"); + return 0; +} + +APS5_EXPORT("Kh6bS2HQKbo", sceHttpUnknown_Kh6bS2HQKbo); +int APS5_VABI sceHttpUnknown_Kh6bS2HQKbo(void) { + NotImplemented_nid_no_patch("Kh6bS2HQKbo"); + return 0; +} + +APS5_EXPORT("U5ExQGyyx9s", sceHttpUnknown_U5ExQGyyx9s); +int APS5_VABI sceHttpUnknown_U5ExQGyyx9s(void) { + NotImplemented_nid_no_patch("U5ExQGyyx9s"); + return 0; +} + +APS5_EXPORT("h9wmFZX4i-4", sceHttpUnknown_h9wmFZX4i_M4); +int APS5_VABI sceHttpUnknown_h9wmFZX4i_M4(void) { + NotImplemented_nid_no_patch("h9wmFZX4i-4"); + return 0; +} + } diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 363c59cde..24b692819 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -803,4 +803,16 @@ int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void return 0; } +APS5_EXPORT("FIjXN2TkuTs", sceJsonUnknown_FIjXN2TkuTs); +int APS5_VABI sceJsonUnknown_FIjXN2TkuTs(void) { + NotImplemented_nid_no_patch("FIjXN2TkuTs"); + return 0; +} + +APS5_EXPORT("i-XwZjw0OOY", sceJsonUnknown_i_MXwZjw0OOY); +int APS5_VABI sceJsonUnknown_i_MXwZjw0OOY(void) { + NotImplemented_nid_no_patch("i-XwZjw0OOY"); + return 0; +} + } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index bd02f7958..86fdaaf97 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1048,4 +1048,10 @@ int APS5_VABI sceNetResolverGetError(int rid, int* status) { return 0; } +APS5_EXPORT("AzqoBha7js4", sceNetUnknown_AzqoBha7js4); +int APS5_VABI sceNetUnknown_AzqoBha7js4(void) { + NotImplemented_nid_no_patch("AzqoBha7js4"); + return 0; +} + } diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 6b2bd01d9..ef3fe2cfd 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -79,4 +79,10 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { return 0; } +APS5_EXPORT("IXmfUaze9So", sceNpCommerceUnknown_IXmfUaze9So); +int APS5_VABI sceNpCommerceUnknown_IXmfUaze9So(void) { + NotImplemented_nid_no_patch("IXmfUaze9So"); + return 0; +} + } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 31fa46049..b8f84d100 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -144,4 +144,10 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { return 0; } +APS5_EXPORT("eDXKe9FndlE", sceNpEntitlementAccessPftUnknown_eDXKe9FndlE); +int APS5_VABI sceNpEntitlementAccessPftUnknown_eDXKe9FndlE(void) { + NotImplemented_nid_no_patch("eDXKe9FndlE"); + return 0; +} + } diff --git a/core/libs/prx/libSceNpPartner001/Export.cpp b/core/libs/prx/libSceNpPartner001/Export.cpp new file mode 100644 index 000000000..ef5b77a1f --- /dev/null +++ b/core/libs/prx/libSceNpPartner001/Export.cpp @@ -0,0 +1,17 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_EXPORT("7CxI50-xlCk", sceNpPartnerUnknown_7CxI50_MxlCk); +int APS5_VABI sceNpPartnerUnknown_7CxI50_MxlCk(void) { + NotImplemented_nid_no_patch("7CxI50-xlCk"); + return 0; +} + +APS5_EXPORT("pMxXhNozUX8", sceNpPartnerUnknown_pMxXhNozUX8); +int APS5_VABI sceNpPartnerUnknown_pMxXhNozUX8(void) { + NotImplemented_nid_no_patch("pMxXhNozUX8"); + return 0; +} + +} diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp new file mode 100644 index 000000000..49b35a64f --- /dev/null +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_EXPORT("Asuucx2vvaE", sceProprietaryVoiceChatHelperUnknown_Asuucx2vvaE); +int APS5_VABI sceProprietaryVoiceChatHelperUnknown_Asuucx2vvaE(void) { + NotImplemented_nid_no_patch("Asuucx2vvaE"); + return 0; +} + +APS5_EXPORT("LDjk9ULlN34", sceProprietaryVoiceChatHelperUnknown_LDjk9ULlN34); +int APS5_VABI sceProprietaryVoiceChatHelperUnknown_LDjk9ULlN34(void) { + NotImplemented_nid_no_patch("LDjk9ULlN34"); + return 0; +} + +APS5_EXPORT("QJy6V9QAXXE", sceProprietaryVoiceChatHelperUnknown_QJy6V9QAXXE); +int APS5_VABI sceProprietaryVoiceChatHelperUnknown_QJy6V9QAXXE(void) { + NotImplemented_nid_no_patch("QJy6V9QAXXE"); + return 0; +} + +APS5_EXPORT("lrswogWNZyM", sceProprietaryVoiceChatHelperUnknown_lrswogWNZyM); +int APS5_VABI sceProprietaryVoiceChatHelperUnknown_lrswogWNZyM(void) { + NotImplemented_nid_no_patch("lrswogWNZyM"); + return 0; +} + +} diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 2e8ee6e64..dcace7377 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -105,4 +105,58 @@ int APS5_VABI scePsmlUnknown_jEevBXmagOQ() { return 0; } +APS5_EXPORT("0GAw7SmkwII", scePsmlMfsr2Unknown_0GAw7SmkwII); +int APS5_VABI scePsmlMfsr2Unknown_0GAw7SmkwII(void) { + NotImplemented_nid_no_patch("0GAw7SmkwII"); + return 0; +} + +APS5_EXPORT("1ic5q-kdOsc", scePsmlMfsr2Unknown_1ic5q_MkdOsc); +int APS5_VABI scePsmlMfsr2Unknown_1ic5q_MkdOsc(void) { + NotImplemented_nid_no_patch("1ic5q-kdOsc"); + return 0; +} + +APS5_EXPORT("5z2gBlqxJ+0", scePsmlMfsr2Unknown_5z2gBlqxJ_P0); +int APS5_VABI scePsmlMfsr2Unknown_5z2gBlqxJ_P0(void) { + NotImplemented_nid_no_patch("5z2gBlqxJ+0"); + return 0; +} + +APS5_EXPORT("Kyy1baXgaVU", scePsmlMfsr2Unknown_Kyy1baXgaVU); +int APS5_VABI scePsmlMfsr2Unknown_Kyy1baXgaVU(void) { + NotImplemented_nid_no_patch("Kyy1baXgaVU"); + return 0; +} + +APS5_EXPORT("ZLL31lzzxr4", scePsmlMfsr2Unknown_ZLL31lzzxr4); +int APS5_VABI scePsmlMfsr2Unknown_ZLL31lzzxr4(void) { + NotImplemented_nid_no_patch("ZLL31lzzxr4"); + return 0; +} + +APS5_EXPORT("gMduXCLYrNg", scePsmlMfsr2Unknown_gMduXCLYrNg); +int APS5_VABI scePsmlMfsr2Unknown_gMduXCLYrNg(void) { + NotImplemented_nid_no_patch("gMduXCLYrNg"); + return 0; +} + +APS5_EXPORT("lrJwpLjKXRc", scePsmlMfsr2Unknown_lrJwpLjKXRc); +int APS5_VABI scePsmlMfsr2Unknown_lrJwpLjKXRc(void) { + NotImplemented_nid_no_patch("lrJwpLjKXRc"); + return 0; +} + +APS5_EXPORT("m9JLPc3wOQw", scePsmlMfsr2Unknown_m9JLPc3wOQw); +int APS5_VABI scePsmlMfsr2Unknown_m9JLPc3wOQw(void) { + NotImplemented_nid_no_patch("m9JLPc3wOQw"); + return 0; +} + +APS5_EXPORT("o+NM86gEwFE", scePsmlMfsr2Unknown_o_PNM86gEwFE); +int APS5_VABI scePsmlMfsr2Unknown_o_PNM86gEwFE(void) { + NotImplemented_nid_no_patch("o+NM86gEwFE"); + return 0; +} + } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 0f824d139..f689bd624 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -209,4 +209,28 @@ int APS5_VABI sceVoiceSetMuteFlag() { return 0; } +APS5_EXPORT("Z6QV6j7igvE", sceVoiceUnknown_Z6QV6j7igvE); +int APS5_VABI sceVoiceUnknown_Z6QV6j7igvE(void) { + NotImplemented_nid_no_patch("Z6QV6j7igvE"); + return 0; +} + +APS5_EXPORT("lLkJVewQK68", sceVoiceUnknown_lLkJVewQK68); +int APS5_VABI sceVoiceUnknown_lLkJVewQK68(void) { + NotImplemented_nid_no_patch("lLkJVewQK68"); + return 0; +} + +APS5_EXPORT("udAxvCePkUs", sceVoiceUnknown_udAxvCePkUs); +int APS5_VABI sceVoiceUnknown_udAxvCePkUs(void) { + NotImplemented_nid_no_patch("udAxvCePkUs"); + return 0; +} + +APS5_EXPORT("xHSNCLrQq3A", sceVoiceUnknown_xHSNCLrQq3A); +int APS5_VABI sceVoiceUnknown_xHSNCLrQq3A(void) { + NotImplemented_nid_no_patch("xHSNCLrQq3A"); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index b2281ac47..ca9308012 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -97,4 +97,10 @@ void APS5_VABI pthread_yield_nid_postfix(void) { std::this_thread::yield(); } +APS5_EXPORT("0wu33hunNdE", posixUnknown_0wu33hunNdE); +int APS5_VABI posixUnknown_0wu33hunNdE(void) { + NotImplemented_nid_no_patch("0wu33hunNdE"); + return 0; +} + } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 99abb9b7e..732e31113 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -30,6 +30,18 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info +- [iE8trxPKnAg](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut2) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [S-Gni2KIJRY](../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [JBN6N-EY+3M, Kh6bS2HQKbo, U5ExQGyyx9s, h9wmFZX4i-4](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [FIjXN2TkuTs, i-XwZjw0OOY](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [AzqoBha7js4](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [IXmfUaze9So](../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [eDXKe9FndlE](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccessPft) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [Asuucx2vvaE, LDjk9ULlN34, QJy6V9QAXXE, lrswogWNZyM](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) (libSceProprietaryVoiceChatHelper) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [Z6QV6j7igvE, lLkJVewQK68, udAxvCePkUs, xHSNCLrQq3A](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [0wu33hunNdE](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libScePosix) - unknown names and signatures, imported by PPSA23566; declared without parameters - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented From 69973800a79bc9d805012166dde0391c7cc58642 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 16:36:52 -0500 Subject: [PATCH 133/203] fix(libkernel): return EINVAL for unknown mutex attribute types UFC 6 (PPSA23566) passes type 0 to pthread_mutexattr_settype during startup. libkernel rejects it with EINVAL and leaves the attribute unchanged, as fpPS4's decompiled body does, instead of aborting. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 3 ++- core/libs/tests/GuestKernelErrors.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 976569adb..67f3ad67b 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,5 +1,6 @@ #include "../include/Pthread.hpp" #include "../include/Mutex.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include #include @@ -114,7 +115,7 @@ int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { case 2: (*attr)->type = MutexType::Recursive; break; case 3: (*attr)->type = MutexType::Normal; break; case 4: (*attr)->type = MutexType::Adaptive; break; - default: throw std::invalid_argument("Invalid mutex type"); + default: return SCE_KERNEL_ERROR_EINVAL; } return 0; } diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index e29493539..9a16733f3 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -83,6 +83,8 @@ int main() { PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); Require(scePthreadMutexattrSettype(&attr, MUTEX_TYPE_ERRORCHECK) == SCE_OK); + Require(scePthreadMutexattrSettype(&attr, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(&attr, 5) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_NONE) == SCE_OK); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_INHERIT) == SCE_OK); bool protectionRejected = false; From e9c8664d4326b2721294cd907455d331fd0c2247 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 16:43:02 -0500 Subject: [PATCH 134/203] fix(libkernel): refuse fixed no-overwrite mappings over live memory with ENOMEM MAP_FIXED | MAP_NO_OVERWRITE over a range that already holds an allocation fails with ENOMEM and leaves the mapping in place, as in fpPS4's TVirtualManager.check_fixed. UFC 6 (PPSA23566) relies on it to probe for a free address and aborted on the throw. Co-Authored-By: Claude Opus 5.5 --- .../prx/libc/include/GuestAllocations.hpp | 2 ++ core/libs/prx/libc/src/GuestAllocations.cpp | 10 +++++++ .../libkernel/DirectMemory/DirectMemory.cpp | 22 +++++++++++++++ core/libs/tests/GuestMemory.cpp | 27 ++++++++++++++++--- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index a292f5d6e..81c711b9e 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -30,6 +30,7 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); +bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, const std::function& apply); @@ -60,6 +61,7 @@ public: void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); } + bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); } Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); } void Remove(const void* pointer) { GuestAllocationsRemove_nid_postfix(handle, pointer); } void Unmap(const void* pointer, std::size_t bytes, const std::function& apply) { GuestAllocationsUnmap_nid_postfix(handle, pointer, bytes, apply); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 64c246c2a..344fcaa82 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -257,6 +257,16 @@ void GuestAllocationsRequireAvailable_nid_postfix(void*, const void* pointer, st } } +bool GuestAllocationsOverlaps_nid_postfix(void*, const void* pointer, std::size_t bytes) { + const auto address = reinterpret_cast(pointer); + require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); + for (const auto& [base, range] : registry().ranges) { + if (base >= address + bytes) break; + if (base + range->bytes > address) return true; + } + return false; +} + bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast(pointer); require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 6a8b32f5f..ca25c8be1 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -565,6 +565,26 @@ void ValidateOutput(void** addr) { } +bool Reserved(const void* addr, size_t len) { + const auto start = reinterpret_cast(addr); + const auto end = start + len; + std::lock_guard lock(g_reservationLock); + auto it = g_reservations.upper_bound(start); + if (it == g_reservations.begin()) return false; + auto cursor = start; + for (--it; it != g_reservations.end() && it->first <= cursor; ++it) { + cursor = std::max(cursor, it->second); + if (cursor >= end) return true; + } + return false; +} + +bool FixedNoOverwriteConflict(const GuestAllocations::Mutation& mutation, void* addr, size_t len, int flags) { + constexpr int GuestMapNoOverwrite = 0x80; + if (addr == nullptr || (flags & GuestMapFixedFlag) == 0 || (flags & GuestMapNoOverwrite) == 0) return false; + return mutation.Overlaps(addr, len) && !Reserved(addr, len); +} + int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; @@ -572,6 +592,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return SCE_KERNEL_ERROR_EINVAL; } GuestAllocations::Mutation mutation; + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) { EraseReservations(*addr, len); RecordProtection(*addr, len, prot); @@ -600,6 +621,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { EraseReservations(*addr, len); RecordProtection(*addr, len, prot); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 59b3fb0ac..3978f90ff 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -111,10 +111,9 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMapFlexibleMemory(&hinted, length, 3, 0) == 0); Require(hinted > first && (reinterpret_cast(hinted) & 0x3fff) == 0); static_cast(hinted)[length - 1] = 1; - bool rejected = false; void* overwrite = first; - try { sceKernelMapFlexibleMemory(&overwrite, 0x4000, 3, 0x90); } catch (const std::exception&) { rejected = true; } - Require(rejected && overwrite == first); + Require(sceKernelMapFlexibleMemory(&overwrite, 0x4000, 3, 0x90) == static_cast(0x8002000cu)); + Require(overwrite == first); Require(sceKernelMunmap(hinted, length) == 0); void* exclusive = hinted; Require(sceKernelMapFlexibleMemory(&exclusive, length, 3, 0x90) == 0); @@ -366,6 +365,27 @@ static void CheckReservedRangeIsNotCommitted() { Require(sceKernelReleaseDirectMemory(phys, page) == 0); } +static void CheckNoOverwriteRefusesLiveMapping() { + constexpr std::size_t page = 0x4000; + constexpr int outOfMemory = static_cast(0x8002000cu); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page, 3, 0, phys, 0) == 0); + static_cast(mapped)[0] = 13; + void* again = mapped; + Require(sceKernelMapDirectMemory(&again, page, 3, 0x90, phys + page, 0) == outOfMemory); + Require(again == mapped); + void* flexible = mapped; + Require(sceKernelMapFlexibleMemory(&flexible, page, 3, 0x90) == outOfMemory); + Require(static_cast(mapped)[0] == 13); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && info.offset == static_cast(phys)); + Require(sceKernelMunmap(mapped, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + #ifdef _WIN32 static void CheckNoOverwriteRejectsHostOccupiedMapping() { constexpr std::size_t page = 0x4000; @@ -857,6 +877,7 @@ int main() { CheckReleaseDirectMemoryClearsMappings(); CheckFixedVirtualReservation(); CheckReservedRangeIsNotCommitted(); + CheckNoOverwriteRefusesLiveMapping(); CheckMlock(); CheckSharedDirectMemoryLifecycle(); CheckGetDirectMemoryType(); From e289b56e6f858b3daa65f7e4fbd54ed692d7848b Mon Sep 17 00:00:00 2001 From: JAB <68183649+JAB-dev@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:10:30 +0200 Subject: [PATCH 135/203] feat(libSceNgs2.native): implement sceNgs2RackGetInfo, sceNgs2VoiceGetPortInfo and sceNgs2VoiceQueryInfo (#958) RackGetInfo reports the rack's handles, buffer, id, uid, option limits, render count and active voices. VoiceGetPortInfo reports a port's matrix, volume and destination. VoiceQueryInfo answers info id 0x4001 with the voice channel count and throws for other ids. Assumptions are recorded in TechnicalDebt. Co-authored-by: Claude Opus 5.5 --- .../libSceNgs2.native/include/Ngs2Types.hpp | 41 ++++++++++++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 6 ++ core/libs/prx/libSceNgs2.native/src/Rack.cpp | 36 +++++++++++ .../libs/prx/libSceNgs2.native/src/System.cpp | 1 - core/libs/prx/libSceNgs2.native/src/Voice.cpp | 27 ++++++++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 62 ++++++++++++++++++- .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 3 + docs/dev/TechnicalDebt.md | 3 + 8 files changed, 176 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 97a0825fb..a6a92c819 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -255,6 +255,30 @@ struct Ngs2CustomSubmixerRackOption { }; static_assert(sizeof(Ngs2CustomSubmixerRackOption) == 1288); +struct Ngs2RackInfo { + char name[64]; + Ngs2Handle rack_handle; + Ngs2ContextBufferInfo buffer_info; + Ngs2Handle owner_system_handle; + std::uint32_t type; + std::uint32_t rack_id; + std::uint32_t uid; + std::uint32_t min_grain_samples; + std::uint32_t max_grain_samples; + std::uint32_t max_voices; + std::uint32_t max_channel_works; + std::uint32_t max_inputs; + std::uint32_t max_matrices; + std::uint32_t max_ports; + std::uint32_t state_flags; + float last_process_ratio; + std::uint64_t last_processed_tick; + std::uint64_t render_count; + std::uint32_t active_voice_count; + std::uint32_t active_channel_work_count; +}; +static_assert(sizeof(Ngs2RackInfo) == 216); + struct Ngs2VoiceParamHeader { std::uint16_t size; std::int16_t next; @@ -437,6 +461,23 @@ struct Ngs2RenderBufferInfo { std::uint32_t num_channels; }; +static constexpr std::uint32_t SCE_NGS2_VOICE_INFO_CHANNELS = 0x4001; + +struct Ngs2VoiceChannelsInfo { + std::uint32_t num_channels; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2VoiceChannelsInfo) == 8); + +struct Ngs2VoicePortInfo { + std::int32_t matrix_id; + float volume; + std::uint32_t num_delay_samples; + std::uint32_t dest_input_id; + Ngs2Handle dest_handle; +}; +static_assert(sizeof(Ngs2VoicePortInfo) == 24); + struct Ngs2VoiceState { std::uint32_t state_flags; std::int32_t error_code; diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index f49ba05ef..8b3965619 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -13,6 +13,7 @@ #include "prx/libSceNgs2.native/include/Ngs2Types.hpp" static constexpr std::uint32_t NGS2_MAX_CHANNELS = 8; +static constexpr std::uint32_t MIN_GRAIN_SAMPLES = 64; struct Ngs2System; struct Ngs2Rack; @@ -117,8 +118,13 @@ struct Ngs2Voice { struct Ngs2Rack { Ngs2System* system = nullptr; std::uint32_t rackId = 0; + std::uint32_t uid = 0; + char name[64] = {}; std::uint32_t maxChannels = 0; std::uint32_t maxFilters = 0; + std::uint32_t maxGrainSamples = 0; + std::uint32_t maxChannelWorks = 0; + std::uint32_t maxInputs = 0; Ngs2ContextBufferInfo bufferInfo{}; Ngs2BufferAllocator allocator{}; std::vector voices; diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 1c0ce54f9..04d922208 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -92,6 +92,7 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackOptions& options, const Ngs2ContextBufferInfo& bufferInfo, const Ngs2BufferAllocator& allocator, Ngs2Handle* handle) { + static std::uint32_t nextUid = 1; auto* system = Ngs2FindSystem(systemHandle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; auto* rack = new (Ngs2Place(&bufferInfo, sizeof(Ngs2Rack), alignof(Ngs2Rack))) Ngs2Rack{}; @@ -99,6 +100,13 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO rack->rackId = rackId; rack->maxChannels = RackMaxChannels(rackId, options); rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0; + rack->uid = nextUid++; + std::memcpy(rack->name, options.common.name, sizeof(rack->name)); + rack->name[sizeof(rack->name) - 1] = '\0'; + rack->maxGrainSamples = options.common.max_grain_samples; + if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->maxChannelWorks = options.sampler.max_channel_works; + if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->maxInputs = options.submixer.max_inputs; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) rack->maxInputs = options.customSubmixer.max_inputs; rack->bufferInfo = bufferInfo; rack->allocator = allocator; rack->voices.resize(options.common.max_voices); @@ -182,6 +190,34 @@ int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id return SCE_NGS2_OK; } +int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (info_size != sizeof(Ngs2RackInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + std::lock_guard lock(Ngs2Mutex()); + const auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + *info = {}; + std::memcpy(info->name, rack->name, sizeof(info->name)); + info->rack_handle = rack_handle; + info->buffer_info = rack->bufferInfo; + info->owner_system_handle = reinterpret_cast(rack->system); + info->type = rack->rackId >> 12; + info->rack_id = rack->rackId; + info->uid = rack->uid; + info->min_grain_samples = MIN_GRAIN_SAMPLES; + info->max_grain_samples = rack->maxGrainSamples; + info->max_voices = static_cast(rack->voices.size()); + info->max_channel_works = rack->maxChannelWorks; + info->max_inputs = rack->maxInputs; + info->max_matrices = static_cast(rack->voices.front().matrices.size()); + info->max_ports = static_cast(rack->voices.front().ports.size()); + info->state_flags = 1; + info->render_count = static_cast(rack->system->renderCount); + info->active_voice_count = static_cast(std::count_if(rack->voices.begin(), rack->voices.end(), + [](const Ngs2Voice& voice) { return voice.state != Ngs2PlayState::Empty; })); + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2RackLock(uintptr_t rack_handle) { Ngs2Mutex().lock(); if (Ngs2FindRack(rack_handle) != nullptr) return SCE_NGS2_OK; diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 468dcc39a..f03e5c7c2 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -13,7 +13,6 @@ #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" -static constexpr std::uint32_t MIN_GRAIN_SAMPLES = 64; static std::vector& Systems() { static std::vector systems; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 37e571613..af69ff6fa 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -347,6 +347,33 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state } } +int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs2VoicePortInfo* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (info_size != sizeof(Ngs2VoicePortInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + std::lock_guard lock(Ngs2Mutex()); + auto& voice = CheckedVoice(voice_handle); + const auto& source = PortAt(voice, port); + *info = {}; + info->matrix_id = source.matrix; + info->volume = source.volume; + info->dest_handle = reinterpret_cast(source.dest); + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t voice_handle, uint32_t info_id, void* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + std::lock_guard lock(Ngs2Mutex()); + const auto& voice = CheckedVoice(voice_handle); + switch (info_id) { + case SCE_NGS2_VOICE_INFO_CHANNELS: { + if (info_size != sizeof(Ngs2VoiceChannelsInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + *static_cast(info) = {voice.channels, 0}; + return SCE_NGS2_OK; + } + default: throw std::runtime_error("NGS2: voice info " + Ngs2Hex(info_id) + " is not implemented"); + } +} + int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_flags) { if (state_flags == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; std::lock_guard lock(Ngs2Mutex()); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 59c576ca2..3aa1d2cb3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -42,11 +42,46 @@ static void TestErrorsAndInfo() { Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A0261u)); const auto system = CreateSystem(); - CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + Ngs2RackInfo rackInfo{}; + Require(sceNgs2RackGetInfo(0x1234, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2RackGetInfo(rack, nullptr, sizeof(rackInfo)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_OK); + Require(rackInfo.rack_handle == rack && rackInfo.owner_system_handle == system && rackInfo.rack_id == SCE_NGS2_RACK_ID_SAMPLER && rackInfo.type == 1); + Require(rackInfo.uid != 0 && rackInfo.max_voices == 256 && rackInfo.max_channel_works == 256 && rackInfo.max_grain_samples == 512); + Require(rackInfo.max_ports == 8 && rackInfo.max_matrices == 1 && rackInfo.active_voice_count == 0 && rackInfo.name[0] == '\0'); + Ngs2SubmixerRackOption named{}; + named.rack_option.size = sizeof(named); + for (auto& c : named.rack_option.name) c = 'n'; + named.rack_option.max_grain_samples = 256; + named.rack_option.max_voices = 2; + named.rack_option.max_matrices = 1; + named.rack_option.max_ports = 2; + named.max_channels = 2; + named.max_inputs = 3; + Ngs2ContextBufferInfo namedQuery{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SUBMIXER, &named.rack_option, &namedQuery) == SCE_NGS2_OK); + const auto namedBuffer = Buffer(namedQuery); + uintptr_t namedRack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SUBMIXER, &named.rack_option, &namedBuffer, &namedRack) == SCE_NGS2_OK); + Ngs2RackInfo namedInfo{}; + Require(sceNgs2RackGetInfo(namedRack, &namedInfo, sizeof(namedInfo)) == SCE_NGS2_OK); + Require(namedInfo.name[0] == 'n' && namedInfo.name[62] == 'n' && namedInfo.name[63] == '\0'); + Require(namedInfo.type == 2 && namedInfo.rack_id == SCE_NGS2_RACK_ID_SUBMIXER && namedInfo.uid != rackInfo.uid && namedInfo.max_grain_samples == 256); + Require(namedInfo.max_voices == 2 && namedInfo.max_ports == 2 && namedInfo.max_inputs == 3 && namedInfo.max_channel_works == 0); + Require(namedInfo.buffer_info.host_buffer == namedBuffer.host_buffer && namedInfo.render_count == 0); + uintptr_t named0 = 0; + Require(sceNgs2RackGetVoiceHandle(namedRack, 0, &named0) == SCE_NGS2_OK); + Control(named0, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Event(named0, SCE_NGS2_VOICE_EVENT_PLAY); + Require(sceNgs2RackGetInfo(namedRack, &namedInfo, sizeof(namedInfo)) == SCE_NGS2_OK && namedInfo.active_voice_count == 1); + Ngs2VoiceChannelsInfo stereoInfo{}; + Require(sceNgs2VoiceQueryInfo(named0, SCE_NGS2_VOICE_INFO_CHANNELS, &stereoInfo, sizeof(stereoInfo)) == SCE_NGS2_OK && stereoInfo.num_channels == 2); Require(sceNgs2SystemGetInfo(system, nullptr, sizeof(info)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Require(sceNgs2SystemGetInfo(system, &info, sizeof(info) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); - Require(info.system_handle == system && info.uid != 0 && info.rack_count == 1 && info.sample_rate == 48000); + Require(info.system_handle == system && info.uid != 0 && info.rack_count == 2 && info.sample_rate == 48000); Require(info.num_grain_samples == Grain && info.max_grain_samples == 512 && info.render_count == 0); Ngs2ContextBufferInfo released{}; @@ -61,6 +96,29 @@ static void TestPcmBlockEnd() { for (int i = 0; i < 12; i++) pcm.push_back(static_cast(i * 1000 - 4000)); const auto sampler = Sampler(system, pcm, 0); Patch(sampler, master); + Ngs2VoicePortInfo portInfo{}; + Require(sceNgs2VoiceGetPortInfo(sampler, 0, nullptr, sizeof(portInfo)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &portInfo, sizeof(portInfo) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &portInfo, sizeof(portInfo)) == SCE_NGS2_OK); + Require(portInfo.dest_handle == master && portInfo.matrix_id == -1 && portInfo.volume == 1.0f); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_VOLUME, Ngs2VoicePortVolumeParam{{}, 0, 0.5f}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &portInfo, sizeof(portInfo)) == SCE_NGS2_OK); + Require(portInfo.dest_handle == master && portInfo.matrix_id == 0 && portInfo.volume == 0.5f); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_VOLUME, Ngs2VoicePortVolumeParam{{}, 0, 1.0f}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, -1}); + Require(sceNgs2VoiceGetPortInfo(sampler, 1, &portInfo, sizeof(portInfo)) == SCE_NGS2_OK && portInfo.dest_handle == 0); + bool portOutOfRange = false; + try { sceNgs2VoiceGetPortInfo(sampler, 8, &portInfo, sizeof(portInfo)); } catch (const std::invalid_argument&) { portOutOfRange = true; } + Require(portOutOfRange); + Ngs2VoiceChannelsInfo channelsInfo{}; + Require(sceNgs2VoiceQueryInfo(sampler, SCE_NGS2_VOICE_INFO_CHANNELS, nullptr, sizeof(channelsInfo)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2VoiceQueryInfo(sampler, SCE_NGS2_VOICE_INFO_CHANNELS, &channelsInfo, sizeof(channelsInfo) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2VoiceQueryInfo(sampler, SCE_NGS2_VOICE_INFO_CHANNELS, &channelsInfo, sizeof(channelsInfo)) == SCE_NGS2_OK); + Require(channelsInfo.num_channels == 1 && channelsInfo.reserved == 0); + bool unknownInfo = false; + try { sceNgs2VoiceQueryInfo(sampler, 0x4000, &channelsInfo, sizeof(channelsInfo)); } catch (const std::runtime_error&) { unknownInfo = true; } + Require(unknownInfo); Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); Require(Flags(sampler) == 0); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 0b1429a67..ecfa52ce3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -29,10 +29,13 @@ int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, cons int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2BufferAllocator*, uintptr_t*); int APS5_VABI sceNgs2RackDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t, uint32_t, uintptr_t*); +int APS5_VABI sceNgs2RackGetInfo(uintptr_t, Ngs2RackInfo*, size_t); int APS5_VABI sceNgs2VoiceControl(uintptr_t, const Ngs2VoiceParamHeader*); int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t, const Ngs2VoiceCommand*, size_t); int APS5_VABI sceNgs2VoiceGetState(uintptr_t, Ngs2VoiceState*, size_t); int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t, uint32_t*); +int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); +int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); } inline void Check(bool value, int line) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8e7b1b60d..827cc3ae2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -140,6 +140,9 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` +- [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero +- [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw +- [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) From baa202d241337ddf4bdf1f0e871a23a219f53dc2 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:32:01 +0200 Subject: [PATCH 136/203] feat(agc): implement 10_11_11 unorm images through 32-bit uint views GFX10 image format 30 (10_11_11_UNORM: X and Y are unorm11 in bits 0-21, Z is unorm10 in bits 22-31, per Mesa's gfx10 tables) has no Vulkan format, so its sampled and storage descriptors threw at decode. Format 30 now takes the R32_UINT route of 10_11_11_UINT: the driver binds the raw dwords as R32_UINT and the shader extracts each field and converts it with the buffer format helpers (NormalizeFormatComponent on loads, EmitFormatStoreComponent on stores); DST_SEL 1 reads 1.0f. Samples, gathers, LOD queries, 16-bit data and loads or stores whose DST_SEL names the missing W channel throw. Converted textures are now viewed with an identity component mapping, since the shader applies DST_SEL itself: a 10_11_11_UINT view whose X selector was not X lost the packed dword. DCC clear codes 0001 and 1110 of a converted texture throw instead of filling one R32_UINT channel. --- .../Graphics/include/TextureFormat.hpp | 1 + .../Graphics/src/DccMetadata.cpp | 3 + .../Graphics/src/ShaderResources.cpp | 11 +- .../Graphics/src/TextureFormat.cpp | 6 +- .../libSceAgcDriver/tests/TextureFormat.cpp | 30 ++ .../tests/execution/ImageConvertedUnorm.cpp | 295 ++++++++++++++++++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 5 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 46 ++- docs/dev/TechnicalDebt.md | 2 + 9 files changed, 387 insertions(+), 12 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index b23f2f1da..aca377fc8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -14,6 +14,7 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); +bool IsConvertedTextureFormat(std::uint32_t guestFormat); bool IsBlockCompressed(std::uint32_t guestFormat); std::uint32_t BlockWidth(std::uint32_t guestFormat); std::uint32_t BlockHeight(std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 1f8c2d1d8..463093477 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include @@ -427,6 +429,7 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue if (resource.dccAddress == 0) return DccKeys::Uncompressed; const auto keys = readDccKeys(resource.dccAddress, guestBytes, memoized); if (keys == DccKeys::Uncompressed) return keys; + if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); std::byte probe[16]{}; if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) { static std::mutex reportedMutex; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d69c35f..dd1147c6f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -48,6 +48,11 @@ VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) { } } +VkComponentMapping ViewComponents(const GuestTextureResource& resource) { + if (IsConvertedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + return {ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)}; +} + // Sampled textures are reused across draws and dispatches while their guest bytes are unchanged; a // byte copy of the guest surface validates each reuse, so CPU or GPU writes to it force a re-upload. // The key is everything a lookup matches: the device, the eight descriptor words and the swizzle. @@ -1697,7 +1702,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const auto resource = DecodeTextureResource(words); - const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)}; + const VkComponentMapping components = ViewComponents(resource); if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; ++textureIndex; } @@ -2438,7 +2443,7 @@ bool ShaderResources::precollectImages() { record.decoded = true; if (record.sampled && !noRecords && words.size() == 8 && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) { std::copy(words.begin(), words.end(), record.words.begin()); - record.components = {ComponentSwizzleFor(record.resource.dstSelX), ComponentSwizzleFor(record.resource.dstSelY), ComponentSwizzleFor(record.resource.dstSelZ), ComponentSwizzleFor(record.resource.dstSelW)}; + record.components = ViewComponents(record.resource); record.keys = TextureClearKeys(record.resource, record.guestBytes); auto& cache = Textures(); std::lock_guard lock(cache.mutex); @@ -2526,7 +2531,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); - const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)}; + const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; std::shared_ptr texture; if (record != nullptr && record->texture != nullptr) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 62bde9b8c..108bdbe5d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -92,7 +92,7 @@ constexpr auto MakeFormatLookupTable() { constexpr auto kFormatLookupTable = MakeFormatLookupTable(); std::uint32_t remapGuestFormat(std::uint32_t guestFormat) { - return guestFormat == 34 ? 20 : guestFormat; + return guestFormat == 30 || guestFormat == 34 ? 20 : guestFormat; } const FormatEntry& findFormatEntry(std::uint32_t guestFormat) { @@ -119,6 +119,10 @@ std::uint32_t BytesPerElement(std::uint32_t guestFormat) { return findFormatEntry(guestFormat).bytesPerElement; } +bool IsConvertedTextureFormat(std::uint32_t guestFormat) { + return remapGuestFormat(guestFormat) != guestFormat; +} + std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes) { switch (format) { case VK_FORMAT_B8G8R8A8_UNORM: format = VK_FORMAT_R8G8B8A8_UNORM; break; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 6fa05cd45..3973bfa18 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -1,5 +1,10 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" +#include +#include #include #include @@ -18,6 +23,23 @@ void reject(TAction action, std::string_view reason) { throw std::runtime_error(std::string("expected texture format rejection: ") + std::string(reason)); } +void convertedDccClearTests() { + alignas(64) std::array keys{}; + GuestTextureResource resource{}; + resource.dccAddress = reinterpret_cast(keys.data()); + const auto read = [&](std::uint32_t format, std::uint8_t key) { + keys.fill(key); + resource.format = format; + return TextureClearKeys(resource, keys.size() * 256u); + }; + for (const std::uint32_t format : {30u, 34u}) { + reject([&] { read(format, 0x40); }, "DCC clear code 0001 of converted texture format " + std::to_string(format)); + reject([&] { read(format, 0x80); }, "DCC clear code 1110 of converted texture format " + std::to_string(format)); + Require(read(format, 0x00) == DccKeys::Clear0000 && read(format, 0xc0) == DccKeys::Clear1111, "converted texture format " + std::to_string(format) + " must keep its 0000 and 1111 DCC clear codes"); + } + Require(read(20, 0x40) == DccKeys::Clear0001 && read(20, 0x80) == DccKeys::Clear1110, "format 20 must keep its 0001 and 1110 DCC clear codes"); +} + } void RunTextureFormatTests() { @@ -45,10 +67,18 @@ void RunTextureFormatTests() { Require(ResolveTextureFormat(34) == ResolveTextureFormat(20), "format 34 must remap to format 20"); Require(BytesPerElement(34) == BytesPerElement(20), "remapped format 34 must share the width of format 20"); + Require(ResolveTextureFormat(30) == ResolveTextureFormat(20), "format 30 must remap to format 20"); + Require(BytesPerElement(30) == 4u, "remapped format 30 must be four bytes wide"); + for (std::uint32_t format = 0; format < 512u; ++format) { + const auto remapped = static_cast(ShaderRecompiler::RemapTextureFormat(static_cast(format))); + if (remapped == format) continue; + Require(ResolveTextureFormat(format) == ResolveTextureFormat(remapped), "guest format " + std::to_string(format) + " must resolve like the format the recompiler remaps it to"); + } reject([] { ResolveTextureFormat(0); }, "unsupported guest texture format"); reject([] { ResolveTextureFormat(183); }, "unsupported guest texture format"); reject([] { ResolveTextureFormat(9999); }, "unsupported guest texture format"); reject([] { BytesPerElement(2); }, "unsupported guest texture format"); reject([] { IsBlockCompressed(200); }, "unsupported guest texture format"); + convertedDccClearTests(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp new file mode 100644 index 000000000..376473101 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -0,0 +1,295 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t LoadWidth = 2048; +constexpr std::uint32_t LoadGroups = LoadWidth / Threads; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t UnormFormat = 30; +constexpr std::uint32_t UintFormat = 34; +constexpr std::uint32_t SwizzleXYZ1 = 0x3acu; +constexpr std::uint32_t SwizzleXZY1 = 0x374u; +constexpr std::uint32_t SwizzleZYX1 = 0x32eu; +constexpr std::uint32_t SwizzleX011 = 0x244u; +constexpr std::uint32_t SwizzleYXZ1 = 0x3a5u; +constexpr std::uint32_t SwizzleXYZW = 0xfacu; +constexpr std::uint32_t D16 = 0x80000000u; +constexpr std::uint32_t DivisionUlps = 3; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t RoundTripOffset = 16384; +constexpr std::size_t StoreOffset = 32768; +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + +alignas(256) std::array Output{}; +alignas(256) std::array Input{}; + +constexpr std::array LoadCode(std::uint32_t dmask, std::uint32_t d16) { + return { + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x7e3e0280u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0xf0001008u | (dmask << 8u), 0x00010a1eu | d16, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, + }; +} + +constexpr std::array StoreCode(std::uint32_t dmask, std::uint32_t d16) { + return { + 0x34060084u, 0xe0381000u, 0x80030a03u, 0x7e3c0300u, 0x7e3e0280u, 0xbf8c3f70u, 0xf0201008u | (dmask << 8u), 0x00010a1eu | d16, + 0xbf810000u, + }; +} + +constexpr std::array SamplerCode(std::uint32_t word0) { + return { + 0x34060084u, 0x7e280280u, 0x7e2a0280u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, + 0xbf810000u, + }; +} + +alignas(256) constexpr std::array RoundTrip{ + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x7e3e0280u, 0xf0001708u, 0x00010a1eu, 0xbf8c3f70u, 0xf0201708u, + 0x00040a1eu, 0xbf810000u, +}; + +alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu, 0u); +alignas(256) constexpr auto LoadXyz = LoadCode(0x7u, 0u); +alignas(256) constexpr auto LoadXy = LoadCode(0x3u, 0u); +alignas(256) constexpr auto LoadD16 = LoadCode(0xfu, D16); +alignas(256) constexpr auto StoreXyz = StoreCode(0x7u, 0u); +alignas(256) constexpr auto StoreXz = StoreCode(0x5u, 0u); +alignas(256) constexpr auto StoreD16 = StoreCode(0x7u, D16); +alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); +alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); +alignas(256) constexpr auto GetLod = SamplerCode(0xf1800308u); + +constexpr std::array StoreValues{ + 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3eaaaaabu, 0x80000000u, 0xbf800000u, + 0x40000000u, 0x7f800000u, 0x7fc00000u, 0x000116c2u, 0x3f7fbe77u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image converted unorm: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle, std::uint32_t width) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u), + (width - 1u) >> 2u, + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::uint32_t LoadTexel(std::uint32_t index) { + return index | ((2047u - index) << 11u) | ((index & 1023u) << 22u); +} + +std::uint32_t Field(std::uint32_t texel, std::uint32_t component) { + return component == 2u ? texel >> 22u : (texel >> (component * 11u)) & 2047u; +} + +struct Expected { + std::uint32_t bits; + std::uint32_t ulps; +}; + +Expected Selected(std::uint32_t texel, std::uint32_t swizzle, std::uint32_t component, bool unorm) { + const auto selector = (swizzle >> (component * 3u)) & 7u; + if (selector == 0u) return {0u, 0u}; + if (selector == 1u) return {unorm ? 0x3f800000u : 1u, 0u}; + const auto field = Field(texel, selector - 4u); + const auto maximum = selector == 6u ? 1023u : 2047u; + if (!unorm) return {field, 0u}; + return {std::bit_cast(static_cast(static_cast(field) / maximum)), field == 0u || field == maximum ? 0u : DivisionUlps}; +} + +std::uint32_t StoreField(std::uint32_t bits, std::uint32_t maximum) { + const auto value = std::bit_cast(bits); + if (!(value > 0.0f)) return 0u; + if (value >= 1.0f) return maximum; + return static_cast(std::nearbyint(static_cast(value) * maximum)); +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, const std::array& extra, std::uint32_t groups, const std::array& target = {}) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(extra.begin(), extra.end(), userData.begin() + 12); + std::copy(target.begin(), target.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void CheckLoad(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, std::span code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { + Run(device, code, TextureDescriptor(texels, format, swizzle, LoadWidth), {}, LoadGroups); + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + std::uint32_t slot = 0; + for (std::uint32_t component = 0; component < 4u; ++component) { + if (((dmask >> component) & 1u) == 0u) continue; + const auto expected = Selected(LoadTexel(index), swizzle, component, format == UnormFormat); + const auto actual = Output[index * 4u + slot++]; + const auto distance = actual > expected.bits ? actual - expected.bits : expected.bits - actual; + Require(distance <= expected.ulps, what + ": texel " + std::to_string(index) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected.bits) + " within " + std::to_string(expected.ulps) + " ULP"); + } + } +} + +void CheckRoundTrip(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + auto* target = texels + RoundTripOffset; + std::fill(target, target + LoadWidth * 4u, static_cast(0xa5u)); + Run(device, RoundTrip, TextureDescriptor(texels, UnormFormat, SwizzleXYZ1, LoadWidth), {}, LoadGroups, TextureDescriptor(target, UnormFormat, SwizzleXYZ1, LoadWidth)); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(target), LoadWidth * 4u, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + std::uint32_t actual = 0; + std::memcpy(&actual, target + index * 4u, 4u); + Require(actual == LoadTexel(index), "image_load then image_store: texel " + std::to_string(index) + " is " + Hex(actual) + ", expected " + Hex(LoadTexel(index))); + } +} + +void CheckStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t dmask, const std::string& what) { + std::fill(texels, texels + Threads * 4u, static_cast(0xa5u)); + Input.fill(0u); + std::array expected{}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array values{StoreValues[tid % 11u], StoreValues[(tid + 4u) % 11u], StoreValues[(tid + 8u) % 11u]}; + std::uint32_t slot = 0; + for (std::uint32_t component = 0; component < 3u; ++component) { + if (((dmask >> component) & 1u) == 0u) continue; + Input[tid * 4u + slot++] = values[component]; + expected[tid] |= StoreField(values[component], component == 2u ? 1023u : 2047u) << (component * 11u); + } + } + Run(device, code, TextureDescriptor(texels, UnormFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), 1); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), Threads * 4u, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::uint32_t actual = 0; + std::memcpy(&actual, texels + tid * 4u, 4u); + Require(actual == expected[tid], what + ": texel " + std::to_string(tid) + " is " + Hex(actual) + ", expected " + Hex(expected[tid])); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what) { + std::string refusal; + try { + Run(device, code, TextureDescriptor(texels, UnormFormat, swizzle, LoadWidth), extra, 1); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(reason) != std::string::npos, what + " of a 10_11_11_UNORM image was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + const auto texel = LoadTexel(index); + std::memcpy(texels + index * 4u, &texel, 4u); + } + CheckLoad(*device, texels, LoadXyzw, 0xfu, UnormFormat, SwizzleXYZ1, "image_load dmask:0xf X Y Z 1"); + CheckLoad(*device, texels, LoadXy, 0x3u, UnormFormat, SwizzleXZY1, "image_load dmask:0x3 X Z Y 1"); + CheckLoad(*device, texels, LoadXyzw, 0xfu, UnormFormat, SwizzleZYX1, "image_load dmask:0xf Z Y X 1"); + CheckLoad(*device, texels, LoadXyzw, 0xfu, UnormFormat, SwizzleX011, "image_load dmask:0xf X 0 1 1"); + CheckLoad(*device, texels, LoadXyz, 0x7u, UintFormat, SwizzleYXZ1, "image_load of 10_11_11_UINT dmask:0x7 Y X Z 1"); + CheckRoundTrip(*device, texels); + CheckStore(*device, texels + StoreOffset, StoreXyz, 0x7u, "image_store dmask:0x7"); + CheckStore(*device, texels + StoreOffset, StoreXz, 0x5u, "image_store dmask:0x5"); + RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_sample_lz"); + RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_gather4_lz"); + RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "queries the level of detail of a converted unorm image", "image_get_lod"); + RequireRefused(*device, LoadD16, texels, SwizzleXYZ1, {}, "converted unorm image with 16-bit data", "image_load d16"); + RequireRefused(*device, StoreD16, texels + StoreOffset, SwizzleXYZ1, BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), "converted unorm image with 16-bit data", "image_store d16"); + RequireRefused(*device, LoadXyzw, texels, SwizzleXYZW, {}, "selects a channel the converted image format does not have", "image_load with DST_SEL X Y Z W"); + RequireRefused(*device, StoreXyz, texels + StoreOffset, SwizzleXYZW, BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), "selects a channel the converted image format does not have", "image_store with DST_SEL X Y Z W"); + std::puts("image converted unorm tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index acc3b3135..2d68f1a1a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,7 +13,7 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, {IrBufferFormat::Format8SNorm, false, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, @@ -34,6 +34,7 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format16_16UInt, true, true, false}, {IrBufferFormat::Format16_16SInt, true, false, true}, {IrBufferFormat::Format16_16Float, true, false, false}, + {IrBufferFormat::Format11_11_10UNorm, true, true, false}, {IrBufferFormat::Format11_11_10UInt, true, true, false}, {IrBufferFormat::Format11_11_10Float, true, false, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, @@ -118,7 +119,7 @@ IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { } IrBufferFormat RemapTextureFormat(IrBufferFormat format) { - return format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; + return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; } std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 419bf78ed..30a34d432 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -417,24 +417,43 @@ SpirvBufferFormatInfo ImageConversionFormat(const ImageResource& image) { return {}; } const auto info = GetFormatInfo(format); - if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || info.type != SpirvFormatComponentType::Uint || !info.packedBitfield || info.byteSize != sizeof(std::uint32_t) || info.componentCount == 0u || info.componentCount > 4u) { - throw std::runtime_error("image conversion format is not a packed 32-bit unsigned integer format"); + if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || (info.type != SpirvFormatComponentType::Uint && info.type != SpirvFormatComponentType::Unorm) || !info.packedBitfield || info.byteSize != sizeof(std::uint32_t) || info.componentCount == 0u || info.componentCount > 4u) { + throw std::runtime_error("image conversion format is not a packed 32-bit unsigned integer or unorm format"); } return info; } +void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SpirvBufferFormatInfo& info) { + if (access.mem.dataBits == 16u) { + ctx.Fail(access.inst, "reads or writes a converted unorm image with 16-bit data, which is not implemented"); + } + for (std::uint32_t component = 0; component < 4u; component++) { + const auto selector = (access.image.shaderSwizzle >> (component * 3u)) & 7u; + if (selector >= 4u && selector - 4u >= info.componentCount) { + ctx.Fail(access.inst, "selects a channel the converted image format does not have"); + } + } +} + std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t texel) { auto& state = ctx.state; const auto info = ImageConversionFormat(access.image); if (info.format == IrBufferFormat::Invalid) { return texel; } + const bool unorm = info.type == SpirvFormatComponentType::Unorm; + if (unorm) { + RequireConvertedUnormAccess(ctx, access, info); + } const auto packed = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), packed, texel, 0u); std::uint32_t components[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0)}; for (std::uint32_t component = 0; component < info.componentCount; component++) { components[component] = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), components[component], packed, ConstantU32(state, info.componentBitOffset[component]), ConstantU32(state, info.componentBits[component])); + if (unorm) { + components[component] = NormalizeFormatComponent(state, info, component, components[component]); + } } for (std::uint32_t component = info.componentCount; component < 4u; component++) { components[component] = components[component % info.componentCount]; @@ -444,7 +463,7 @@ std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess for (std::uint32_t component = 0; component < 4u; component++) { const auto selector = (swizzle >> (component * 3u)) & 7u; if (selector == 1u) { - selected[component] = ConstantU32(state, 1u); + selected[component] = ConstantU32(state, FormattedConstantBits(info, SpirvFormattedSourceKind::One)); } else if (selector >= 4u) { selected[component] = components[selector - 4u]; } else { @@ -572,13 +591,22 @@ std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& if (info.format == IrBufferFormat::Invalid) { return texel; } + const bool unorm = info.type == SpirvFormatComponentType::Unorm; + if (unorm) { + RequireConvertedUnormAccess(ctx, access, info); + } auto packed = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < info.componentCount; component++) { const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, texel, component); - const auto maximum = ConstantU32(state, info.componentBits[component] == 32u ? UINT32_MAX : (1u << info.componentBits[component]) - 1u); - const auto within = Binary(state, spv::OpULessThan, TypeBool(state), value, maximum); - const auto clamped = Select(state, TypeU32(state), within, value, maximum); + auto clamped = value; + if (unorm) { + clamped = EmitFormatStoreComponent(state, info, component, value); + } else { + const auto maximum = ConstantU32(state, info.componentBits[component] == 32u ? UINT32_MAX : (1u << info.componentBits[component]) - 1u); + const auto within = Binary(state, spv::OpULessThan, TypeBool(state), value, maximum); + clamped = Select(state, TypeU32(state), within, value, maximum); + } const auto shifted = info.componentBitOffset[component] == 0u ? clamped : Binary(state, spv::OpShiftLeftLogical, TypeU32(state), clamped, ConstantU32(state, info.componentBitOffset[component])); packed = Binary(state, spv::OpBitwiseOr, TypeU32(state), packed, shifted); } @@ -695,6 +723,9 @@ void EmitQueryDimensionsOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; + if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { + ctx.Fail(access.inst, "queries the level of detail of a converted unorm image, which is not implemented"); + } state.module.EmitCapability(spv::CapabilityImageQuery); const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); const auto coord = CoordF32(ctx, access, 0, RdnaImageDimensionInfoFor(access.image.dimension).spatialComponents, AddressDimension(access).spatialComponents); @@ -795,6 +826,9 @@ SampleSetup MakeSampleSetup(SpirvValueEmitContext& ctx, const ImageEmitAccess& a if (dref && access.image.conversionFormat != IrBufferFormat::Invalid) { ctx.Fail(access.inst, "uses depth comparison with a packed integer image"); } + if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { + ctx.Fail(access.inst, "samples or gathers a converted unorm image, which needs filtering in the shader and is not implemented"); + } const auto coord = CoordF32(ctx, access, layout.coord, dimensionInfo.coordinateComponents, AddressDimension(access).coordinateComponents); return {dimensionInfo, layout, access.image.numericClass, dref, coord}; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 827cc3ae2..bf6bfd94b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -193,6 +193,8 @@ Throughout the project, every function at every stage either **does exactly what - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. - [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented; half-border addressing, the border color table, gradients, LOD bias, offsets, LOD clamps, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. +- [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. +- [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped From a40fe0b41537ed4c6175b0734d70eabbd95e456e Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:58:13 +0200 Subject: [PATCH 137/203] fix(shader): throw for constant lane reads outside the host subgroup A pixel, vertex or tessellation stage whose wave is wider than the host subgroup runs one guest lane per invocation, so lanes at or above the subgroup size have no invocation. v_readlane_b32 still shuffled from them, which Vulkan leaves undefined; a wave64 pixel minimum loop that reads lanes 31 and 63 can then wait for a value no live lane holds and hang the GPU. At one lane per invocation outside compute, a v_readlane_b32 of a constant lane at or above the host subgroup size, and v_permlanex16_b32 on a subgroup of 16 lanes or fewer, now fail SPIR-V emission. Lanes in registers still build, so s_ff1_i32_b64 waterfalls are unaffected, and neither check applies to compute or to two lanes per invocation. v_permlane16_b32 and v_permlanex16_b32 take the source lane's EXEC from a ballot, as DPP does, instead of a shuffle from a possibly inactive invocation. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 54 +++++++++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 20 ++++++- .../SpirvBackend/src/SpirvEmitter.cpp | 1 + docs/dev/TechnicalDebt.md | 2 + 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a65c7d9e5..0345dbccf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -552,6 +552,59 @@ void verifyProgramCounterRelativeData() { require(moved.cacheHit, "program counter data: relocating the shader recompiled it"); require(dataBase(moved) == codeAddress + 0x1000u + 56u, "program counter data: the relocated shader bound the old address"); } + +void verifyLanesOutsideHostSubgroup() { + using namespace ShaderRecompiler; + const auto pixel = [](std::span code, std::uint32_t subgroupSize) { + ShaderPixelStageInfo info{}; + info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + info.hasPerspectiveCenterVgpr = true; + info.targetOutputMode[0] = 9u; + info.targetExportMapping.fill(0xe4u); + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = info; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return !Recompile(request).spirv.empty(); + }; + const auto compute = [](std::span code, std::uint32_t subgroupSize) { + static constexpr std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return !Recompile(request).spirv.empty(); + }; + static constexpr std::array lane63{0xd7600006u, 0x00017f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array lane31{0xd7600006u, 0x00013f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array waterfall{0xbe80047eu, 0xbe821400u, 0xd7600003u, 0x00000500u, 0xbe801c02u, 0xbf138000u, 0xbf85fffau, 0x7e020203u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array computeLane63{0xd7600006u, 0x00017f00u, 0x7e020206u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + static constexpr std::array permlanex16{0xd7780001u, 0x02010100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array permlane16{0xd7770001u, 0x02010100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + expectFailure([&] { static_cast(pixel(lane63, 32u)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup that runs this wave64 program at one lane per invocation", "host lanes: a wave64 pixel program read lane 63 of a 32-lane subgroup"); + expectFailure([&] { static_cast(pixel(lane63, 8u)); }, "v_readlane_b32 of lane 63 is outside the 8-lane host subgroup", "host lanes: a wave64 pixel program read lane 63 of an 8-lane subgroup"); + require(pixel(lane31, 32u), "host lanes: a wave64 pixel program did not build reading lane 31 of a 32-lane subgroup"); + expectFailure([&] { static_cast(pixel(lane31, 8u)); }, "v_readlane_b32 of lane 31 is outside the 8-lane host subgroup", "host lanes: a wave64 pixel program read lane 31 of an 8-lane subgroup"); + require(pixel(lane63, 64u), "host lanes: a wave64 pixel program did not build reading lane 63 of a 64-lane subgroup"); + require(compute(computeLane63, 32u), "host lanes: a wave64 compute program did not build reading lane 63 at two lanes per invocation"); + require(compute(computeLane63, 8u), "host lanes: a wave64 compute program did not build reading lane 63 of an 8-lane subgroup"); + require(pixel(waterfall, 32u) && pixel(waterfall, 8u), "host lanes: an s_ff1_i32_b64 waterfall reading a lane in a register did not build"); + expectFailure([&] { static_cast(pixel(permlanex16, 8u)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "host lanes: v_permlanex16_b32 read lanes 16-31 of an 8-lane subgroup"); + require(pixel(permlanex16, 32u) && pixel(permlane16, 8u), "host lanes: a v_permlane16_b32 row that the host subgroup may hold did not build"); +} + void verifyMeshConfiguration() { using namespace ShaderRecompiler; ShaderMeshInputInfo list; @@ -1042,6 +1095,7 @@ int main() { verifyBindlessTable(); verifyDescriptorPhis(); verifyProgramCounterRelativeData(); + verifyLanesOutsideHostSubgroup(); verifyMeshConfiguration(); verifyPixelInputs(); verifyPixelParameterSlots(); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1e761cf73..8e074215d 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -82,6 +82,7 @@ struct SpirvEmitterState { SpirvRequirements requirements; std::uint32_t laneCount = 1; + std::uint32_t hostSubgroupSize = 0; bool splitSubgroup = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index ec5d995ea..dd8c915aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -216,6 +217,15 @@ std::uint32_t EmitDsMaskedLaneRead(SpirvEmitterState& state, std::uint32_t sourc return Select(state, TypeU32(state), sourceActive, shuffled, ConstantU32(state, 0u)); } +bool HostSubgroupNarrowerThanWave(const SpirvEmitterState& state) { + return state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u && state.hostSubgroupSize < state.program.WaveSize(); +} + +[[noreturn]] void FailOutsideHostSubgroup(const SpirvValueEmitContext& ctx, const IrValue& inst, const std::string& access) { + const auto& state = ctx.state; + ctx.Fail(inst, (access + " is outside the " + std::to_string(state.hostSubgroupSize) + "-lane host subgroup that runs this wave" + std::to_string(state.program.WaveSize()) + " program at one lane per invocation").c_str()); +} + } std::uint32_t EmitConvertU16U32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -547,6 +557,11 @@ std::uint32_t EmitReadFirstLane(SpirvValueEmitContext& ctx, const IrValue& inst) std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; + const IrValue* selector = inst.Argument(1)->Resolve(); + if (selector != nullptr && selector->HasImmediate() && HostSubgroupNarrowerThanWave(state)) { + const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); + if (index >= state.hostSubgroupSize) FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + } const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, state.program.WaveSize() - 1u)); return ctx.Shuffle(inst, 0, lane); } @@ -562,6 +577,7 @@ std::uint32_t EmitWriteLane(SpirvValueEmitContext& ctx, const IrValue& inst) { std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto flags = inst.Flags(); + if (flags.x16 && state.hostSubgroupSize <= 16u && HostSubgroupNarrowerThanWave(state)) FailOutsideHostSubgroup(ctx, inst, "v_permlanex16_b32 from lanes 16-31"); const auto subid = EmitSubgroupLocalInvocationId(state); const auto row = state.module.AllocateId(); const auto rowValue = state.module.AllocateId(); @@ -591,9 +607,9 @@ std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst) if (flags.fetchInactive) { return shuffled; } - const auto sourceExec = ctx.Shuffle(inst, 3, target); + const auto sourceActive = EmitBallotLaneActiveBool(state, ctx.Ballot(inst.Argument(3)), target); const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpSelect, TypeU32(state), result, sourceExec, shuffled, flags.boundControl ? ConstantU32(state, 0u) : ctx.Arg(inst, 4)); + state.module.AddFunction(spv::OpSelect, TypeU32(state), result, sourceActive, shuffled, flags.boundControl ? ConstantU32(state, 0u) : ctx.Arg(inst, 4)); return result; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8dcc53b53..596d281b1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -248,6 +248,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.supportedCapabilities = target.supportedCapabilities; state.supportedExtensions = target.supportedExtensions; state.nonConstantImageOffsets = target.nonConstantImageOffsets; + state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; const auto* workgroup = ShaderWorkgroupInputFor(state); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 827cc3ae2..0530603d8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -167,6 +167,8 @@ Throughout the project, every function at every stage either **does exactly what - [`ds_wrxchg2_rtn_b32` and `ds_wrxchg2st64_rtn_b32`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. +- [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. +- Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. From df98b8899ee6ac3c65956bb6b27181f5df60dc27 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 17:28:39 -0500 Subject: [PATCH 138/203] feat(libs): name the PPSA23566 stubs from the shadPS4 NID list and implement sleep 20 of the 31 NIDs have names in shadPS4's aerolib list. They are declared under those names; POSIX sleep is implemented. The NpPartner001 and Psml MFSR2 exports stay unnamed. Co-Authored-By: Claude Opus 5.5 --- .../libSceAudioOut/src/AudioOut2System.cpp | 5 ++--- core/libs/prx/libSceConvertKeycode/Export.cpp | 5 ++--- core/libs/prx/libSceHttp/Export.cpp | 20 ++++++++----------- core/libs/prx/libSceJson2/Export.cpp | 10 ++++------ core/libs/prx/libSceNet/Export.cpp | 5 ++--- core/libs/prx/libSceNpCommerce/Export.cpp | 5 ++--- .../prx/libSceNpEntitlementAccess/Export.cpp | 5 ++--- .../Export.cpp | 20 ++++++++----------- core/libs/prx/libSceVoice/Export.cpp | 20 ++++++++----------- .../prx/libkernel/Pthread/Posix/Thread.cpp | 6 +++--- docs/dev/TechnicalDebt.md | 11 +--------- 11 files changed, 42 insertions(+), 70 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 754cb5382..67d66b44b 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -29,9 +29,8 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar return 0; } -APS5_EXPORT("iE8trxPKnAg", sceAudioOut2Unknown_iE8trxPKnAg); -int APS5_VABI sceAudioOut2Unknown_iE8trxPKnAg(void) { - NotImplemented_nid_no_patch("iE8trxPKnAg"); +int APS5_VABI sceAudioOut2UserGetSupportedAttributes(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index cc99aa240..2ac60a0cd 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -22,9 +22,8 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint64_t a0, std::uint64_t return kErrInvalidAddress; } -APS5_EXPORT("S-Gni2KIJRY", sceConvertKeycodeUnknown_S_MGni2KIJRY); -int APS5_VABI sceConvertKeycodeUnknown_S_MGni2KIJRY(void) { - NotImplemented_nid_no_patch("S-Gni2KIJRY"); +int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 673332c18..16eca0b4a 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -370,27 +370,23 @@ int APS5_VABI sceHttpGetLastErrno(int request_id, int* errno_out) { return 0; } -APS5_EXPORT("JBN6N-EY+3M", sceHttpUnknown_JBN6N_MEY_P3M); -int APS5_VABI sceHttpUnknown_JBN6N_MEY_P3M(void) { - NotImplemented_nid_no_patch("JBN6N-EY+3M"); +int APS5_VABI sceHttpsGetSslError(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Kh6bS2HQKbo", sceHttpUnknown_Kh6bS2HQKbo); -int APS5_VABI sceHttpUnknown_Kh6bS2HQKbo(void) { - NotImplemented_nid_no_patch("Kh6bS2HQKbo"); +int APS5_VABI sceHttpSetCookieRecvCallback(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("U5ExQGyyx9s", sceHttpUnknown_U5ExQGyyx9s); -int APS5_VABI sceHttpUnknown_U5ExQGyyx9s(void) { - NotImplemented_nid_no_patch("U5ExQGyyx9s"); +int APS5_VABI sceHttpsSetSslVersion(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("h9wmFZX4i-4", sceHttpUnknown_h9wmFZX4i_M4); -int APS5_VABI sceHttpUnknown_h9wmFZX4i_M4(void) { - NotImplemented_nid_no_patch("h9wmFZX4i-4"); +int APS5_VABI sceHttpSetRedirectCallback(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 24b692819..0d7e33f95 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -803,15 +803,13 @@ int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void return 0; } -APS5_EXPORT("FIjXN2TkuTs", sceJsonUnknown_FIjXN2TkuTs); -int APS5_VABI sceJsonUnknown_FIjXN2TkuTs(void) { - NotImplemented_nid_no_patch("FIjXN2TkuTs"); +int APS5_VABI _ZN3sce4Json5Value5clearEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("i-XwZjw0OOY", sceJsonUnknown_i_MXwZjw0OOY); -int APS5_VABI sceJsonUnknown_i_MXwZjw0OOY(void) { - NotImplemented_nid_no_patch("i-XwZjw0OOY"); +int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 86fdaaf97..c93975c43 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1048,9 +1048,8 @@ int APS5_VABI sceNetResolverGetError(int rid, int* status) { return 0; } -APS5_EXPORT("AzqoBha7js4", sceNetUnknown_AzqoBha7js4); -int APS5_VABI sceNetUnknown_AzqoBha7js4(void) { - NotImplemented_nid_no_patch("AzqoBha7js4"); +int APS5_VABI sceNetResolverAbort(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index ef3fe2cfd..525178f39 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -79,9 +79,8 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { return 0; } -APS5_EXPORT("IXmfUaze9So", sceNpCommerceUnknown_IXmfUaze9So); -int APS5_VABI sceNpCommerceUnknown_IXmfUaze9So(void) { - NotImplemented_nid_no_patch("IXmfUaze9So"); +int APS5_VABI sceNpCommerceDialogOpen2(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index b8f84d100..61f27d29e 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -144,9 +144,8 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { return 0; } -APS5_EXPORT("eDXKe9FndlE", sceNpEntitlementAccessPftUnknown_eDXKe9FndlE); -int APS5_VABI sceNpEntitlementAccessPftUnknown_eDXKe9FndlE(void) { - NotImplemented_nid_no_patch("eDXKe9FndlE"); +int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp index 49b35a64f..882e6e92c 100644 --- a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -2,27 +2,23 @@ extern "C" { -APS5_EXPORT("Asuucx2vvaE", sceProprietaryVoiceChatHelperUnknown_Asuucx2vvaE); -int APS5_VABI sceProprietaryVoiceChatHelperUnknown_Asuucx2vvaE(void) { - NotImplemented_nid_no_patch("Asuucx2vvaE"); +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("LDjk9ULlN34", sceProprietaryVoiceChatHelperUnknown_LDjk9ULlN34); -int APS5_VABI sceProprietaryVoiceChatHelperUnknown_LDjk9ULlN34(void) { - NotImplemented_nid_no_patch("LDjk9ULlN34"); +int APS5_VABI sceProprietaryVoiceChatHelperInitialize(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("QJy6V9QAXXE", sceProprietaryVoiceChatHelperUnknown_QJy6V9QAXXE); -int APS5_VABI sceProprietaryVoiceChatHelperUnknown_QJy6V9QAXXE(void) { - NotImplemented_nid_no_patch("QJy6V9QAXXE"); +int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lrswogWNZyM", sceProprietaryVoiceChatHelperUnknown_lrswogWNZyM); -int APS5_VABI sceProprietaryVoiceChatHelperUnknown_lrswogWNZyM(void) { - NotImplemented_nid_no_patch("lrswogWNZyM"); +int APS5_VABI sceProprietaryVoiceChatHelperTerminate(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index f689bd624..898511c88 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -209,27 +209,23 @@ int APS5_VABI sceVoiceSetMuteFlag() { return 0; } -APS5_EXPORT("Z6QV6j7igvE", sceVoiceUnknown_Z6QV6j7igvE); -int APS5_VABI sceVoiceUnknown_Z6QV6j7igvE(void) { - NotImplemented_nid_no_patch("Z6QV6j7igvE"); +int APS5_VABI sceVoiceGetResourceInfo(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lLkJVewQK68", sceVoiceUnknown_lLkJVewQK68); -int APS5_VABI sceVoiceUnknown_lLkJVewQK68(void) { - NotImplemented_nid_no_patch("lLkJVewQK68"); +int APS5_VABI sceVoiceEnableChat(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("udAxvCePkUs", sceVoiceUnknown_udAxvCePkUs); -int APS5_VABI sceVoiceUnknown_udAxvCePkUs(void) { - NotImplemented_nid_no_patch("udAxvCePkUs"); +int APS5_VABI sceVoiceResetPort(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("xHSNCLrQq3A", sceVoiceUnknown_xHSNCLrQq3A); -int APS5_VABI sceVoiceUnknown_xHSNCLrQq3A(void) { - NotImplemented_nid_no_patch("xHSNCLrQq3A"); +int APS5_VABI sceVoiceDisableChat(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index ca9308012..56e848102 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -6,6 +6,7 @@ #include "../include/Pthread.hpp" #include "Common.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include #include extern "C" { @@ -97,9 +98,8 @@ void APS5_VABI pthread_yield_nid_postfix(void) { std::this_thread::yield(); } -APS5_EXPORT("0wu33hunNdE", posixUnknown_0wu33hunNdE); -int APS5_VABI posixUnknown_0wu33hunNdE(void) { - NotImplemented_nid_no_patch("0wu33hunNdE"); +unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds) { + std::this_thread::sleep_for(std::chrono::seconds(seconds)); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 732e31113..0933e7732 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -30,18 +30,9 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info -- [iE8trxPKnAg](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut2) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [S-Gni2KIJRY](../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [JBN6N-EY+3M, Kh6bS2HQKbo, U5ExQGyyx9s, h9wmFZX4i-4](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [FIjXN2TkuTs, i-XwZjw0OOY](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [AzqoBha7js4](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [IXmfUaze9So](../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [eDXKe9FndlE](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccessPft) - unknown names and signatures, imported by PPSA23566; declared without parameters - [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [Asuucx2vvaE, LDjk9ULlN34, QJy6V9QAXXE, lrswogWNZyM](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) (libSceProprietaryVoiceChatHelper) - unknown names and signatures, imported by PPSA23566; declared without parameters - [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [Z6QV6j7igvE, lLkJVewQK68, udAxvCePkUs, xHSNCLrQq3A](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown names and signatures, imported by PPSA23566; declared without parameters -- [0wu33hunNdE](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libScePosix) - unknown names and signatures, imported by PPSA23566; declared without parameters +- PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented From cc7bda30e79a1e192e74d3b40ad974225113e255 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:40:06 -0400 Subject: [PATCH 139/203] fix(libc): register the application heap finalizer when libc loads The finalizer was registered with std::atexit from the heap handshake, which runs after libc's own __cxa_atexit runner and the title's exit callbacks register theirs. std::atexit runs handlers in reverse registration order, so the heap was torn down first: an exit user that allocated then threw "application heap: allocator has been finalized" out of a destructor path, which terminates the process. Register the finalizer from a namespace-scope object instead, so it is registered when libc is loaded and therefore runs last, and make finalize() idempotent rather than throwing on a second pass. application_heap_exit_order registers a guest exit callback that allocates through the heap and aborts with "application heap: allocator has been finalized" on the previous code. --- core/libs/prx/libc/CMakeLists.txt | 3 ++- core/libs/prx/libc/src/ApplicationHeap.cpp | 13 +++++++++---- core/libs/tests/ApplicationHeap.cpp | 10 ++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 575971f71..f63141384 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -242,8 +242,9 @@ if(BUILD_TESTING) add_test(NAME application_heap_replacement COMMAND application_heap_tests) add_test(NAME application_heap_invalid_metadata COMMAND application_heap_tests invalid) add_test(NAME application_heap_default COMMAND application_heap_tests default) + add_test(NAME application_heap_exit_order COMMAND application_heap_tests exit-order) if(WIN32) - set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default + set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 3cd055106..b24c4fe64 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -95,11 +95,10 @@ void requireAlignment(std::size_t alignment) { } void finalize() { - Initialize finalizeCallback; + Initialize finalizeCallback = nullptr; { std::lock_guard lock(heapMutex); - if (heapFailure) std::rethrow_exception(heapFailure); - if (heapFinalized) throw std::runtime_error("application heap: duplicate finalization"); + if (heapFinalized || heapFailure) return; finalizeCallback = heapFinalize; } if (finalizeCallback != nullptr) finalizeCallback(); @@ -107,6 +106,13 @@ void finalize() { heapFinalized = true; } +struct HeapFinalizerRegistration { + HeapFinalizerRegistration() { + if (std::atexit(finalize) != 0) std::terminate(); + } +}; +const HeapFinalizerRegistration heapFinalizerRegistration; + } void ApplicationHeapRegister_nid_no_patch(void* const* api) { @@ -144,7 +150,6 @@ void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { std::lock_guard lock(heapMutex); heapFinalize = read(replacement, 0x18); } - if (std::atexit(finalize) != 0) throw std::runtime_error("application heap: cannot register finalization"); } catch (...) { std::lock_guard lock(heapMutex); heapFailure = std::current_exception(); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 0f08924bb..088f79122 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -17,6 +17,7 @@ void* ApplicationHeapRealign_nid_no_patch(void*, std::size_t, std::size_t); char* APS5_VABI strdup_nid_postfix(const char*); char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI atexit_nid_postfix(void (APS5_VABI*)()); } namespace { @@ -42,6 +43,9 @@ void reject(TAction action) { void APS5_VABI initialize() { ++initializes; } void APS5_VABI finalize() { require(initializes == 1); } +void APS5_VABI exitCallbackAllocates() { + if (ApplicationHeapAllocate_nid_no_patch(16) != storage.data()) throw std::runtime_error("application heap test failed"); +} void* APS5_VABI allocate(std::size_t bytes) { lastSize = bytes; @@ -110,6 +114,12 @@ int main(int argc, char** argv) { write(replacement, 0x40, &align); write(replacement, 0x48, &realign); write(replacement, 0x50, &posixAlign); + if (argc > 1 && std::strcmp(argv[1], "exit-order") == 0) { + require(atexit_nid_postfix(exitCallbackAllocates) == 0); + ApplicationHeapInitialize_nid_no_patch(process.data()); + require(initializes == 1); + return 0; + } if (argc > 1 && std::strcmp(argv[1], "default") == 0) { std::array partial{}; partial[0] = reinterpret_cast(&allocate); From 09f878237802d038e723cd696696faf2adc69224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <57327458+adriadam10@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:42:29 +0200 Subject: [PATCH 140/203] feat(libSceAudio3d): implement sceAudio3dTerminate (#830) --- core/libs/prx/libSceAudio3d/Export.cpp | 7 +++++++ core/libs/tests/GuestAudio3dPort.cpp | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 705f81119..a31de1c48 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -74,6 +74,13 @@ int APS5_VABI sceAudio3dInitialize(int64_t reserved) { return 0; } +int APS5_VABI sceAudio3dTerminate() { + std::lock_guard lock(g_mutex); + if (!g_initialized || g_port.open) return AUDIO3D_ERROR_NOT_READY; + g_initialized = false; + return 0; +} + int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index c0e58ad57..4eb2488a0 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -9,6 +9,7 @@ extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); int APS5_VABI sceAudio3dInitialize(std::int64_t reserved); +int APS5_VABI sceAudio3dTerminate(); int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, std::uint32_t* id); int APS5_VABI sceAudio3dPortClose(std::uint32_t port_id); int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t attribute_id, const void* attribute, std::size_t attribute_size); @@ -62,6 +63,7 @@ void RequireLevel(std::uint32_t level, std::uint32_t available, const char* mess void CheckBeforeInitialize() { std::uint32_t id = 7; Audio3dOpenParameters parameters = Defaults(); + Require(sceAudio3dTerminate() == NOT_READY, "terminate before initialize must be NOT_READY"); Require(sceAudio3dInitialize(1) == INVALID_PARAMETER, "non-zero reserved must be rejected"); Require(Open(parameters, &id) == NOT_READY, "open before initialize must be NOT_READY"); Require(id == 7, "a failed open must not write the port id"); @@ -177,6 +179,22 @@ void CheckQueue() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckTerminate() { + std::uint32_t id = 7; + const Audio3dOpenParameters parameters = Defaults(); + Require(Open(parameters, &id) == 0 && id == 0, "open before terminate"); + Require(sceAudio3dTerminate() == NOT_READY, "terminate with an open port must be NOT_READY"); + Require(sceAudio3dPortClose(0) == 0, "close before terminate"); + Require(sceAudio3dTerminate() == 0, "terminate"); + Require(sceAudio3dTerminate() == NOT_READY, "double terminate must be NOT_READY"); + id = 7; + Require(Open(parameters, &id) == NOT_READY && id == 7, "open after terminate must be NOT_READY"); + Require(sceAudio3dInitialize(0) == 0, "initialize after terminate"); + Require(Open(parameters, &id) == 0 && id == 0, "open after reinitialize"); + Require(sceAudio3dPortClose(0) == 0, "close after reinitialize"); + Require(sceAudio3dTerminate() == 0, "terminate after reinitialize"); +} + } int main() { @@ -184,4 +202,5 @@ int main() { CheckOpenParameters(); CheckOpenClose(); CheckQueue(); + CheckTerminate(); } From ab9437e34df49c00b32a514a2516e15bd75145db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <57327458+adriadam10@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:32 +0200 Subject: [PATCH 141/203] feat(libkernel): implement scePthreadTestcancel and pthread_testcancel (#834) --- .../prx/libkernel/Pthread/Posix/Thread.cpp | 5 +++++ core/libs/prx/libkernel/Pthread/src/Thread.cpp | 3 +++ core/libs/tests/GuestThreadSelf.cpp | 18 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 27 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index b2281ac47..6057243a1 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -16,6 +16,7 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadRename(Pthread thread, const char* name); Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); +void APS5_VABI scePthreadTestcancel(); int APS5_VABI scePthreadSetprio(Pthread thread, int prio); int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); } @@ -93,6 +94,10 @@ int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, cons return PosixThread::ToErrno(scePthreadSetprio(thread, param->sched_priority)); } +void APS5_VABI pthread_testcancel_nid_postfix(void) { + scePthreadTestcancel(); +} + void APS5_VABI pthread_yield_nid_postfix(void) { std::this_thread::yield(); } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 178ec5b7c..d71ca02e8 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -502,6 +502,9 @@ int APS5_VABI scePthreadSetcanceltype(int type, int* old_type) { return SCE_OK; } +void APS5_VABI scePthreadTestcancel() { +} + int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; thread->priority.store(prio, std::memory_order_relaxed); diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 4290ec703..1358c5045 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -8,6 +8,10 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadDetach(Pthread thread); void APS5_VABI scePthreadExit(void* retval); Pthread APS5_VABI scePthreadSelf(); +void APS5_VABI scePthreadTestcancel(); +void APS5_VABI pthread_testcancel_nid_postfix(void); +int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); +int APS5_VABI scePthreadSetcanceltype(int type, int* old_type); int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); @@ -38,6 +42,8 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int MUTEX_TYPE_RECURSIVE = 2; +static constexpr int PTHREAD_CANCEL_ENABLE = 0; +static constexpr int PTHREAD_CANCEL_ASYNCHRONOUS = 2; static constexpr std::intptr_t WorkerRetval = 0x1234; static void Require(bool value) { if (!value) std::abort(); } @@ -48,6 +54,7 @@ struct WorkerContext { bool workerStackReported = false; PthreadMutex* mutex = nullptr; int unlockResult = 0; + bool testcancelReturned = false; }; static void* APS5_VABI Worker(void* arg) { @@ -56,6 +63,13 @@ static void* APS5_VABI Worker(void* arg) { int local = 0; context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); + int oldState = -1; + int oldType = -1; + context.testcancelReturned = scePthreadSetcancelstate(PTHREAD_CANCEL_ENABLE, &oldState) == SCE_OK && + scePthreadSetcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldType) == SCE_OK; + scePthreadTestcancel(); + pthread_testcancel_nid_postfix(); + context.testcancelReturned = context.testcancelReturned && oldState == PTHREAD_CANCEL_ENABLE; scePthreadExit(reinterpret_cast(WorkerRetval)); return nullptr; } @@ -92,6 +106,10 @@ int main() { Require(context.selfFromWorker != mainSelf); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); Require(context.workerStackReported); + Require(context.testcancelReturned); + + scePthreadTestcancel(); + pthread_testcancel_nid_postfix(); Require(scePthreadMutexUnlock(&mutex) == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f3626c584..5bd3b428a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -165,6 +165,7 @@ Throughout the project, every function at every stage either **does exactly what - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler but nothing delivers to it: a host fault still ends the process, and `sceKernelRaiseException` is not implemented. PPSA12544's PS5Util module (Unity) installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection, so delivery must run the handler on the target thread with a FreeBSD `ucontext` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. +- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. - [`ds_wrxchg2_rtn_b32` and `ds_wrxchg2st64_rtn_b32`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. From ea68e850ff802d8292540a7cf4643299b682e4c3 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:58:45 -0400 Subject: [PATCH 142/203] test(agc): fix the stale 32-bit WriteWait expectation and run the full suite WriteWait has truncated the reference and mask of a 32-bit wait since 9c2a7560 ("relaxed checks for what titles send"), but testMemory still expected a reference above 32 bits to throw, so agc_command_tests failed when run without arguments. ctest only registers the "defaults" mode, so the stale expectation was never noticed. Update the expectation to the truncating behaviour (the packet keeps the low halves) and register the full mode as agc_commands so the packet, marker, flip, register and memory cases are checked again. Closes #1005. --- core/libs/prx/libSceAgc/CMakeLists.txt | 3 ++- core/libs/prx/libSceAgc/tests/Commands.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index d3bb48f0b..cfcf3e4ac 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -72,8 +72,9 @@ configure_windows_unwind(agc_command_tests) if(BUILD_TESTING) set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_initialization COMMAND agc_command_tests defaults) + add_test(NAME agc_commands COMMAND agc_command_tests) if(WIN32) - set_tests_properties(agc_initialization PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_initialization agc_commands PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 4e11a5bed..592be7022 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -331,8 +331,9 @@ void testMemory() { check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word"); const auto before = storage.words; expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); }); - expectFailure([&] { Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0, 32, __func__); }); check(storage.words == before, "invalid memory operation modified packet memory"); + auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__); + check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask"); } void testDefaults() { From 6ac3ad5d1dca6356e87e5469d6fbbb0985f98f4b Mon Sep 17 00:00:00 2001 From: Arjen Singh Date: Tue, 6 Oct 2026 16:18:01 -0700 Subject: [PATCH 143/203] fix(relinker): quote literal autorun paths on POSIX --- core/relinker/cli/src/Autorun.cpp | 9 +++++++++ core/relinker/cli/tests/AutorunExitTests.cpp | 21 ++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index 3bb7016be..0660289e4 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -16,7 +16,16 @@ int Autorun(const std::string& absPath, bool toWindows) { std::filesystem::perm_options::add); } +#ifdef _WIN32 const std::string cmd = "\"" + absPath + "\""; +#else + std::string cmd = "'"; + for (const char character : absPath) { + if (character == '\'') cmd += "'\\''"; + else cmd += character; + } + cmd += '\''; +#endif const int rawCode = std::system(cmd.c_str()); int exitCode = rawCode; diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index f6fc70829..4890de71d 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include int main(int, char* argv[]) { const auto executable = std::filesystem::absolute(argv[0]); @@ -14,7 +15,7 @@ int main(int, char* argv[]) { if (name.find("signal") != std::string::npos) std::raise(SIGILL); return 42; } - const auto child = std::filesystem::temp_directory_path() / ("anyps5-autorun-child-" + + auto child = std::filesystem::temp_directory_path() / ("anyps5-autorun-child-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + " with spaces" + executable.extension().string()); #ifndef _WIN32 const auto signaled = child.parent_path() / ("anyps5-autorun-child-signal-" + child.filename().string()); @@ -24,9 +25,21 @@ int main(int, char* argv[]) { int result = 0; try { std::filesystem::copy_file(executable, child); - for (const bool windows : {false, true}) { - const int code = Cli::Autorun(child.string(), windows); - if (code != 42) throw std::runtime_error("Autorun changed exit code 42 to " + std::to_string(code)); + std::vector suffixes{""}; +#ifndef _WIN32 + suffixes.insert(suffixes.end(), {" $HOME", " $(printf substituted)", " `printf substituted`", + " 'single'", " \"double\"", " \\backslash", " line\nbreak"}); +#endif + const auto originalChild = child; + for (const auto& suffix : suffixes) { + const auto renamed = std::filesystem::path(originalChild.string() + suffix); + if (renamed != child) std::filesystem::rename(child, renamed); + child = renamed; + for (const bool windows : {false, true}) { + const int code = Cli::Autorun(child.string(), windows); + if (code != 42) throw std::runtime_error("Autorun changed exit code 42 to " + + std::to_string(code) + " for " + child.string()); + } } #ifndef _WIN32 std::filesystem::rename(child, signaled); From 26eec5bee6147c643d271bf3fd06061c694ad8e6 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 20:37:54 -0300 Subject: [PATCH 144/203] feat(libSceAgc): implement sceAgcDcbQueueEndOfShaderActionGetSize The end-of-shader action is written by an inline SDK builder as a RELEASE_MEM packet, so the size is the 32 bytes that sceAgcCbReleaseMem writes and that sceAgcAcbQueueEndOfShaderActionGetSize already returns. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 ++++++ .../libSceAgc/DcbState/src/ContextState.cpp | 5 +-- .../libSceAgc/tests/EndOfShaderActionSize.cpp | 40 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index d3bb48f0b..05010683e 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -137,6 +137,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_end_of_shader_action_size_tests EXCLUDE_FROM_ALL tests/EndOfShaderActionSize.cpp) +target_include_directories(agc_end_of_shader_action_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_end_of_shader_action_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_end_of_shader_action_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_end_of_shader_action_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_end_of_shader_action_size_tests COMMAND agc_end_of_shader_action_size_tests) + if(WIN32) + set_tests_properties(agc_end_of_shader_action_size_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL tests/BranchPatch.cpp Command/src/Packet.cpp diff --git a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp index 27f037bd2..8e78f4864 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp @@ -69,9 +69,8 @@ std::uint32_t APS5_VABI sceAgcDcbSetBaseDrawIndirectArgsGetSize() { return 16; } -uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize() { + return 32; } std::uint32_t APS5_VABI sceAgcDcbGetLodStatsGetSize() { diff --git a/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp b/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp new file mode 100644 index 000000000..df935a053 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp @@ -0,0 +1,40 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include + +extern "C" { +std::uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize(); +std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize(); +std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer*, std::uint8_t, std::uint16_t, std::uint8_t, std::uint8_t, const volatile Label*, std::uint8_t, std::uint64_t, std::uint16_t, std::uint16_t, std::uint8_t, std::uint32_t); +} + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +} + +int main() { + try { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcCbReleaseMem(&buffer, 0x2f, 0, 0, 0, nullptr, 0, 0, 0, 0, 0, 0); + const auto releaseBytes = static_cast((buffer.cursor_up - packet) * sizeof(std::uint32_t)); + check(((packet[0] >> 8u) & 0xffu) == 0x49u && releaseBytes == 32, "end-of-shader RELEASE_MEM size mismatch"); + check(sceAgcDcbQueueEndOfShaderActionGetSize() == releaseBytes, "DCB end-of-shader action size differs from its RELEASE_MEM"); + check(sceAgcDcbQueueEndOfShaderActionGetSize() == sceAgcAcbQueueEndOfShaderActionGetSize(), "DCB and ACB end-of-shader action sizes differ"); + LibcRunShutdown_nid_postfix(); + std::puts("AGC end-of-shader action size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5bd3b428a..0012ef22d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -71,6 +71,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `V++UgBtQhn0`; prosper returns header + 2 for both types from that address function - [gQkqkLttcpw](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime +- [sceAgcDcbQueueEndOfShaderActionGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature - [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. Not implemented on Windows - [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified From db49ba97b539046f30b855257be0e2f246d4b33c Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 13:50:39 -0700 Subject: [PATCH 145/203] fix(libc): return posix_memalign allocation errors --- core/libs/prx/libc/src/ApplicationHeap.cpp | 3 ++- core/libs/prx/libc/src/FileAndHeap.cpp | 11 +++++++++- core/libs/tests/ApplicationHeap.cpp | 24 ++++++++++++++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 3cd055106..6c7d93c92 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -221,7 +221,8 @@ int ApplicationHeapPosixAlign_nid_no_patch(void** pointer, std::size_t alignment const auto align = callback(6); CallbackScope scope; void* result = nullptr; - if (align(&result, alignment, bytes) != 0) throw std::runtime_error("application heap: posix_memalign failed"); + const int error = align(&result, alignment, bytes); + if (error != 0) return error; requireAllocation(result); if (reinterpret_cast(result) % alignment != 0) throw std::runtime_error("application heap: allocator returned a misaligned pointer"); *pointer = result; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 80f1716a1..dc3a4208d 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -211,7 +211,16 @@ void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { } int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { - return ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); + if (!pointer || alignment < sizeof(void*) || (alignment & (alignment - 1)) != 0) return 22; + const int savedError = errno; + try { + const int result = ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); + errno = savedError; + return result; + } catch (const std::bad_alloc&) { + errno = savedError; + return 12; + } } void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t count, diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 0f08924bb..da9f30958 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -4,10 +4,12 @@ #include #include #include +#include #include #include extern "C" { +int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*); void APS5_VABI _ZdlPv_nid_postfix(void*); @@ -24,6 +26,7 @@ namespace { alignas(64) std::array storage{}; std::size_t lastSize = 0; std::size_t lastAlignment = 0; +unsigned posixCalls = 0; unsigned initializes = 0; unsigned frees = 0; bool fail = false; @@ -75,7 +78,8 @@ void* APS5_VABI realign(void* pointer, std::size_t bytes, std::size_t alignment) } int APS5_VABI posixAlign(void** pointer, std::size_t alignment, std::size_t bytes) { - if (fail) return 12; + ++posixCalls; + if (fail) { *__error_nid_postfix() = 12; *pointer = nullptr; return 12; } *pointer = align(alignment, bytes); return 0; } @@ -188,9 +192,25 @@ int main(int argc, char** argv) { reject([] { ApplicationHeapAlign_nid_no_patch(4, 64); }); reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); void* unchanged = storage.data(); - reject([&] { ApplicationHeapPosixAlign_nid_no_patch(&unchanged, 64, 64); }); + *__error_nid_postfix() = 77; + try { + const int error = posix_memalign_nid_postfix(&unchanged, 64, 64); + if (error != 12) { std::fprintf(stderr, "posix_memalign callback: expected ENOMEM 12, received %d\n", error); return 1; } + } catch (const std::exception&) { + std::fputs("posix_memalign callback: expected ENOMEM 12, received exception\n", stderr); + return 1; + } + require(*__error_nid_postfix() == 77); require(unchanged == storage.data()); fail = false; + const auto beforeInvalid = posixCalls; + for (const std::size_t alignment : {std::size_t{0}, std::size_t{1}, std::size_t{4}, std::size_t{24}}) { + require(posix_memalign_nid_postfix(&unchanged, alignment, 32) == 22); + require(unchanged == storage.data() && *__error_nid_postfix() == 77); + } + require(posix_memalign_nid_postfix(nullptr, 64, 32) == 22 && posixCalls == beforeInvalid); + require(posix_memalign_nid_postfix(&unchanged, 64, 31) == 0 && unchanged == storage.data()); + require(lastSize == 31 && lastAlignment == 64 && *__error_nid_postfix() == 77); recurse = true; reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); recurse = false; From c6dde16023464136ee84a9a703671bc51e274bf8 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 6 Oct 2026 16:44:58 -0700 Subject: [PATCH 146/203] fix(libc): preserve invalid allocator result diagnostics --- core/libs/prx/libc/src/FileAndHeap.cpp | 11 +++-------- core/libs/tests/ApplicationHeap.cpp | 12 ++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index dc3a4208d..60ed9ef87 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -213,14 +213,9 @@ void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { if (!pointer || alignment < sizeof(void*) || (alignment & (alignment - 1)) != 0) return 22; const int savedError = errno; - try { - const int result = ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); - errno = savedError; - return result; - } catch (const std::bad_alloc&) { - errno = savedError; - return 12; - } + const int result = ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); + errno = savedError; + return result; } void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t count, diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index da9f30958..664104d4f 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include extern "C" { @@ -31,6 +32,7 @@ unsigned initializes = 0; unsigned frees = 0; bool fail = false; bool recurse = false; +bool nullPosixResult = false; void require(bool condition) { if (!condition) throw std::runtime_error("application heap test failed"); @@ -80,6 +82,7 @@ void* APS5_VABI realign(void* pointer, std::size_t bytes, std::size_t alignment) int APS5_VABI posixAlign(void** pointer, std::size_t alignment, std::size_t bytes) { ++posixCalls; if (fail) { *__error_nid_postfix() = 12; *pointer = nullptr; return 12; } + if (nullPosixResult) { *pointer = nullptr; return 0; } *pointer = align(alignment, bytes); return 0; } @@ -203,6 +206,15 @@ int main(int argc, char** argv) { require(*__error_nid_postfix() == 77); require(unchanged == storage.data()); fail = false; + nullPosixResult = true; + bool rejectedNull = false; + try { + const int error = posix_memalign_nid_postfix(&unchanged, 64, 64); + std::fprintf(stderr, "posix_memalign callback: success with null pointer must throw, received %d\n", error); + return 1; + } catch (const std::bad_alloc&) { rejectedNull = true; } + require(rejectedNull && unchanged == storage.data()); + nullPosixResult = false; const auto beforeInvalid = posixCalls; for (const std::size_t alignment : {std::size_t{0}, std::size_t{1}, std::size_t{4}, std::size_t{24}}) { require(posix_memalign_nid_postfix(&unchanged, alignment, 32) == 22); From 72be77590c646b7057384c0afa5fc5e16c7eef48 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Wed, 7 Oct 2026 04:55:18 +0530 Subject: [PATCH 147/203] feat(ajm): implement sceAjmBatchJobSetResampleParameters and sceAjmBatchJobGetResampleInfo Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/Export.cpp | 18 -- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 204 ++++++++++++++++--- core/libs/tests/GuestAjm.cpp | 216 +++++++++++++++++++++ 3 files changed, 390 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 4664c379b..3585560a0 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -38,24 +38,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const return 0; } -int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { - (void)info; - (void)instance; - (void)result; - AjmStub(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { - (void)info; - (void)instance; - (void)ratio; - (void)flags; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 1cd4f3f3e..bff995503 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -100,6 +100,18 @@ struct SidebandAt9CodecInfo { std::uint32_t frameSamples; }; +struct SidebandResampleInfo { + float ratio; + std::int32_t samples; + std::uint32_t reserved[8]; +}; + +struct Resampler { + float ratio = 1.0f; + std::vector frames; + double position = 1.0; +}; + struct Instance { std::uint32_t codec = 0; std::uint64_t flags = 0; @@ -119,6 +131,7 @@ struct Instance { std::uint32_t opusChannels = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; + Resampler resampler; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); @@ -142,6 +155,8 @@ enum class JobKind : std::uint32_t { Run = 4, GetStatistics = 5, Control = 6, + SetResampleParameters = 7, + GetResampleInfo = 8, }; struct JobHeader { @@ -236,7 +251,13 @@ bool ResetDecoder(Instance& instance) { void OpenOpus(Instance& instance); +void ClearResampler(Instance& instance) { + instance.resampler.frames.clear(); + instance.resampler.position = 1.0; +} + std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + ClearResampler(instance); if (instance.codec == CODEC_OPUS) { if (size < 8) return AJM_RESULT_INVALID_PARAMETER; std::uint32_t channels = 0; @@ -336,6 +357,82 @@ std::uint32_t PcmEncoding(const Instance& instance) { return static_cast((instance.flags >> 7u) & 7u); } +double PcmValue(const std::uint8_t* at, std::uint32_t encoding) { + if (encoding == 0) { + std::int16_t value = 0; + std::memcpy(&value, at, sizeof(value)); + return value / 32768.0; + } + if (encoding == 1) { + std::int32_t value = 0; + std::memcpy(&value, at, sizeof(value)); + return value / 2147483648.0; + } + float value = 0; + std::memcpy(&value, at, sizeof(value)); + return value; +} + +void StorePcm(double value, std::uint8_t* out, std::uint32_t encoding) { + if (encoding == 0) { + const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); + std::memcpy(out, &converted, sizeof(converted)); + } else if (encoding == 1) { + const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); + std::memcpy(out, &converted, sizeof(converted)); + } else { + const auto converted = static_cast(value); + std::memcpy(out, &converted, sizeof(converted)); + } +} + +bool Resampling(const Instance& instance) { + return instance.resampler.ratio != 1.0f || !instance.resampler.frames.empty(); +} + +std::size_t ResamplerHeld(const Instance& instance, std::size_t channels) { + const auto& resampler = instance.resampler; + if (resampler.frames.empty() || channels == 0) return 0; + const std::size_t frames = resampler.frames.size() / channels; + const auto next = static_cast(resampler.position); + return frames > next ? frames - next : 0; +} + +void ResamplerPush(Instance& instance, const double* samples, std::size_t frames, std::size_t channels) { + auto& stored = instance.resampler.frames; + if (frames == 0) return; + if (stored.empty()) stored.insert(stored.end(), samples, samples + channels); + stored.insert(stored.end(), samples, samples + frames * channels); +} + +void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t channels, std::uint32_t encoding, std::size_t sampleBytes) { + auto& resampler = instance.resampler; + if (resampler.frames.empty() || channels == 0) return; + const std::size_t frameBytes = channels * sampleBytes; + const std::size_t available = resampler.frames.size() / channels; + std::vector frame(frameBytes); + while (outputs.Room() >= frameBytes) { + const auto index = static_cast(resampler.position); + if (index + 2 >= available) break; + const double t = resampler.position - static_cast(index); + for (std::size_t channel = 0; channel < channels; ++channel) { + const double p0 = resampler.frames[(index - 1) * channels + channel]; + const double p1 = resampler.frames[index * channels + channel]; + const double p2 = resampler.frames[(index + 1) * channels + channel]; + const double p3 = resampler.frames[(index + 2) * channels + channel]; + const double value = p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))); + StorePcm(value, frame.data() + channel * sampleBytes, encoding); + } + outputs.Emit(frame.data(), frameBytes); + resampler.position += resampler.ratio; + } + const auto index = std::min(static_cast(resampler.position), available); + if (index > 1) { + resampler.frames.erase(resampler.frames.begin(), resampler.frames.begin() + static_cast((index - 1) * channels)); + resampler.position -= static_cast(index - 1); + } +} + void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { std::size_t first = 0; std::size_t count = samples; @@ -349,8 +446,16 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; count = static_cast(std::min(count, remaining)); } - outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); instance.totalDecodedSamples += count; + if (!Resampling(instance)) { + outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); + return; + } + const auto encoding = PcmEncoding(instance); + std::vector values(count * channels); + for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(pcm + (first * channels + index) * sampleBytes, encoding); + ResamplerPush(instance, values.data(), count, channels); + ResamplerProduce(instance, outputs, channels, encoding, sampleBytes); } SidebandFormat CurrentFormat(const Instance& instance) { @@ -419,6 +524,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c // reported as partial input, and the title resubmits it from the consumed offset. const auto superframeSize = static_cast(instance.info.superframeSize); const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); for (;;) { if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; @@ -426,8 +532,8 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (pcmOutputs.Room() < frameBytes) { - if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { @@ -647,6 +753,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, instance.mp3Channels, encoding, sampleBytes); while (consumed < input.size()) { Mp3Frame frame{}; if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { @@ -657,8 +764,8 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (pcmOutputs.Room() < frame.samples * frame.channels * sampleBytes) { - if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (pcmOutputs.Room() < (Resampling(instance) ? 1 : frame.samples) * frame.channels * sampleBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } packet->data = const_cast(input.data() + consumed); @@ -704,7 +811,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } - if (consumed == input.size() && frames == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; AJM_TRACE("[ajm] instance %u mp3 run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); @@ -757,12 +864,19 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; + const std::size_t frameBytes = channels * sampleBytes; + if (Resampling(instance) && !instance.opusPending.empty()) { + std::vector values(instance.opusPending.size() / sampleBytes); + for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(instance.opusPending.data() + index * sampleBytes, encoding); + ResamplerPush(instance, values.data(), values.size() / channels, channels); + instance.opusPending.clear(); + } + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); if (!instance.opusPending.empty()) { const std::size_t bytes = std::min(instance.opusPending.size(), pcmOutputs.Room()); pcmOutputs.Emit(instance.opusPending.data(), bytes); instance.opusPending.erase(instance.opusPending.begin(), instance.opusPending.begin() + static_cast(bytes)); } - const std::size_t frameBytes = channels * sampleBytes; while (consumed < input.size() && instance.opusPending.empty()) { if (input.size() - consumed < 2) { result |= AJM_RESULT_PARTIAL_INPUT; @@ -797,24 +911,11 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT) throw std::runtime_error("AJM Opus: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); const auto frameChannels = static_cast(decoded->ch_layout.nb_channels); const auto frameSamples = static_cast(decoded->nb_samples); - pcm.assign(frameSamples * channels * sampleBytes, 0); - for (std::size_t sample = 0; sample < frameSamples; ++sample) { - for (std::size_t channel = 0; channel < channels; ++channel) { - const std::size_t sourceChannel = std::min(channel, frameChannels - 1); - const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] - : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; - auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; - if (encoding == 0) { - const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); - std::memcpy(out, &converted, sizeof(converted)); - } else if (encoding == 1) { - const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); - std::memcpy(out, &converted, sizeof(converted)); - } else { - std::memcpy(out, &value, sizeof(value)); - } - } - } + const auto value = [&](std::size_t sample, std::size_t channel) { + const std::size_t sourceChannel = std::min(channel, frameChannels - 1); + return format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] + : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; + }; std::size_t first = 0; std::size_t count = frameSamples; if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { @@ -827,12 +928,23 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; count = static_cast(std::min(count, remaining)); } - const auto* start = pcm.data() + first * frameBytes; - const std::size_t total = count * frameBytes; - const std::size_t fits = std::min(total, pcmOutputs.Room() / frameBytes * frameBytes); - pcmOutputs.Emit(start, fits); - instance.opusPending.insert(instance.opusPending.end(), start + fits, start + total); instance.totalDecodedSamples += count; + if (Resampling(instance)) { + std::vector values(count * channels); + for (std::size_t sample = 0; sample < count; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) values[sample * channels + channel] = value(first + sample, channel); + } + ResamplerPush(instance, values.data(), count, channels); + ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); + } else { + pcm.assign(count * frameBytes, 0); + for (std::size_t sample = 0; sample < count; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) StorePcm(value(first + sample, channel), pcm.data() + (sample * channels + channel) * sampleBytes, encoding); + } + const std::size_t fits = std::min(pcm.size(), pcmOutputs.Room() / frameBytes * frameBytes); + pcmOutputs.Emit(pcm.data(), fits); + instance.opusPending.insert(instance.opusPending.end(), pcm.begin() + static_cast(fits), pcm.end()); + } av_frame_unref(decoded.get()); } if (status != AVERROR(EAGAIN)) { @@ -854,6 +966,7 @@ void ClearContext(Instance& instance) { if (instance.mp3) avcodec_flush_buffers(instance.mp3); if (instance.opus) avcodec_flush_buffers(instance.opus); instance.opusPending.clear(); + ClearResampler(instance); } std::size_t ControlInitializeSize(std::uint32_t codec) { @@ -884,6 +997,13 @@ void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) WriteResult(job.sideband, job.sidebandSize, result); } +std::size_t HeldSamples(const Instance& instance) { + const std::size_t channels = instance.codec == CODEC_OPUS ? instance.opusChannels : instance.codec == CODEC_MP3 ? instance.mp3Channels : static_cast(std::max(0, instance.info.channels)); + if (Resampling(instance)) return ResamplerHeld(instance, channels); + const std::size_t sampleBytes = PcmEncoding(instance) == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + return channels == 0 ? 0 : instance.opusPending.size() / (channels * sampleBytes); +} + void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -922,6 +1042,17 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out case JobKind::Control: Control(*instance, job, inputs); break; + case JobKind::SetResampleParameters: + std::memcpy(&instance->resampler.ratio, job.parameters, sizeof(instance->resampler.ratio)); + AJM_TRACE("[ajm] instance %u set resample ratio %f\n", job.instance, static_cast(instance->resampler.ratio)); + WriteResult(job.sideband, job.sidebandSize, 0); + break; + case JobKind::GetResampleInfo: { + const SidebandResampleInfo resample{instance->resampler.ratio, static_cast(HeldSamples(*instance)), {}}; + WriteResult(job.sideband, job.sidebandSize, 0); + if (job.sideband && job.sidebandSize >= sizeof(SidebandResult) + sizeof(resample)) std::memcpy(static_cast(job.sideband) + sizeof(SidebandResult), &resample, sizeof(resample)); + break; + } case JobKind::Run: if (!instance->initialized) { AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance); @@ -1094,6 +1225,19 @@ int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); } +int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { + (void)flags; + if (!std::isfinite(ratio) || ratio <= 0.0f) return SCE_AJM_ERROR_INVALID_PARAMETER; + auto header = MakeHeader(JobKind::SetResampleParameters, instance, result, sizeof(SidebandResult)); + std::memcpy(header.parameters, &ratio, sizeof(ratio)); + header.parameterSize = sizeof(ratio); + return Append(info, header, nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { + return Append(info, MakeHeader(JobKind::GetResampleInfo, instance, result, sizeof(SidebandResult) + sizeof(SidebandResampleInfo)), nullptr, nullptr); +} + int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 6d0a6593b..b2878b8a9 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -28,6 +28,9 @@ int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); +int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo*, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo*, std::uint32_t, float, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo*, std::uint32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -663,6 +666,216 @@ void TestControlOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +struct ResampleInfo { + std::int32_t result; + std::int32_t internalResult; + float ratio; + std::int32_t samples; + std::uint32_t reserved[8]; +}; +static_assert(sizeof(ResampleInfo) == 48); + +std::uint32_t CreateOpus(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {2, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), result) == 0); + Submit(context, info); + Require(result[0] == 0); + return instance; +} + +ResampleInfo GetResampleInfo(std::uint32_t context, std::uint32_t instance) { + ResampleInfo resample; + std::memset(&resample, 0xAA, sizeof(resample)); + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetResampleInfo(&info, instance, &resample) == 0); + Submit(context, info); + return resample; +} + +struct ResampledStream { + std::vector pcm; + std::size_t jobs = 0; + std::size_t shortJobs = 0; + ResampleInfo afterFirstJob{}; + ResampleInfo atEnd{}; +}; + +ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std::uint8_t* stream, std::size_t size, std::size_t channels, std::size_t frames, float ratio) { + ResampledStream out; + std::vector pcm(frames * channels); + std::size_t offset = 0; + for (int guard = 0; guard < 4096; ++guard) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t setResult[2] = {-1, -1}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + if (ratio > 0) Require(sceAjmBatchJobSetResampleParameters(&info, instance, ratio, 1, setResult) == 0); + Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + Submit(context, info); + if (ratio > 0) Require(setResult[0] == 0); + Require(sideband.result == 0 || (offset == size && sideband.outputWritten == 0)); + offset += static_cast(sideband.inputConsumed); + if (offset < size && static_cast(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs; + out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast(sizeof(std::int16_t))); + if (++out.jobs == 1) out.afterFirstJob = GetResampleInfo(context, instance); + if (offset == size && sideband.outputWritten == 0) break; + } + Require(offset == size); + out.atEnd = GetResampleInfo(context, instance); + return out; +} + +void RequireDecimated(const std::vector& reference, const std::vector& resampled, std::size_t channels, std::size_t step) { + const std::size_t frames = reference.size() / channels; + Require(resampled.size() / channels == (frames - 3) / step + 1); + for (std::size_t frame = 0; frame < resampled.size() / channels; ++frame) { + for (std::size_t channel = 0; channel < channels; ++channel) Require(resampled[frame * channels + channel] == reference[frame * step * channels + channel]); + } +} + +void RequireInterpolated(const std::vector& reference, const std::vector& resampled, std::size_t channels) { + const std::size_t frames = reference.size() / channels; + Require(resampled.size() / channels == 2 * frames - 4); + const auto at = [&](std::ptrdiff_t frame, std::size_t channel) { return static_cast(reference[static_cast(std::max(frame, 0)) * channels + channel]); }; + for (std::size_t frame = 0; frame < resampled.size() / channels; ++frame) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const auto source = static_cast(frame / 2); + const double midpoint = (-at(source - 1, channel) + 9.0 * at(source, channel) + 9.0 * at(source + 1, channel) - at(source + 2, channel)) / 16.0; + if (frame % 2 == 0) Require(resampled[frame * channels + channel] == reference[frame / 2 * channels + channel]); + else Require(std::fabs(resampled[frame * channels + channel] - midpoint) <= 1.5); + } + } +} + +void TestResampleOpus(std::uint32_t context) { + const std::uint32_t plain = CreateOpus(context); + const auto reference = Stream(context, plain, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 960, 0); + Require(reference.pcm.size() / 2 >= 960 * 4); + + const std::uint32_t unity = CreateOpus(context); + const auto passthrough = Stream(context, unity, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 1.0f); + Require(passthrough.pcm == reference.pcm); + Require(passthrough.afterFirstJob.result == 0 && passthrough.afterFirstJob.internalResult == 0 && passthrough.afterFirstJob.ratio == 1.0f && passthrough.afterFirstJob.samples == 960 - 512); + for (const auto word : passthrough.afterFirstJob.reserved) Require(word == 0); + Require(passthrough.atEnd.samples == 0); + + const std::uint32_t faster = CreateOpus(context); + const auto decimated = Stream(context, faster, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 2.0f); + RequireDecimated(reference.pcm, decimated.pcm, 2, 2); + Require(decimated.afterFirstJob.ratio == 2.0f && decimated.afterFirstJob.samples == 2); + Require(decimated.atEnd.result == 0 && decimated.atEnd.samples >= 0 && decimated.atEnd.samples <= 2); + + const std::uint32_t slower = CreateOpus(context); + const auto interpolated = Stream(context, slower, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 2); + Require(interpolated.shortJobs == 0 && passthrough.shortJobs == 0); + Require(interpolated.afterFirstJob.ratio == 0.5f && interpolated.afterFirstJob.samples == 960 - 256); + Require(interpolated.atEnd.samples >= 0 && interpolated.atEnd.samples <= 2); + + const std::uint32_t hades = CreateOpus(context); + const auto pitched = Stream(context, hades, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.890899f); + const double expected = static_cast(reference.pcm.size() / 2 - 2) / 0.890899; + Require(std::fabs(static_cast(pitched.pcm.size() / 2) - expected) <= 2.0); + + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + for (const float bad : {0.0f, -1.0f, std::nanf(""), INFINITY}) Require(sceAjmBatchJobSetResampleParameters(&info, slower, bad, 1, result) == static_cast(0x80930005)); + Require(info.offset == 0); + + const std::size_t firstPacket = 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)); + const auto clear = [&] { + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobClearContext(&info, slower, result) == 0); + Submit(context, info); + Require(result[0] == 0 && GetResampleInfo(context, slower).samples == 0); + }; + clear(); + const auto before = Stream(context, slower, OPUS_STEREO, firstPacket, 2, 512, 0.5f); + Require(before.pcm.size() / 2 == 2 * 960 - 4); + clear(); + const auto after = Stream(context, slower, OPUS_STEREO, firstPacket, 2, 512, 0.5f); + Require(after.pcm == before.pcm); + + const std::uint32_t switched = CreateOpus(context); + const auto head = Stream(context, switched, OPUS_STEREO, 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)), 2, 512, 1.0f); + Require(head.jobs >= 1 && head.pcm.size() == 960 * 2); + const std::uint32_t resumed = CreateOpus(context); + std::vector pcm(512 * 2); + DecodeSideband first{}; + RunDecode(context, resumed, OPUS_STEREO, sizeof(OPUS_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && first.outputWritten == 512 * 2 * 2 && GetResampleInfo(context, resumed).samples == 960 - 512); + std::vector rest(pcm.begin(), pcm.end()); + const auto tail = Stream(context, resumed, OPUS_STEREO + first.inputConsumed, sizeof(OPUS_STEREO) - static_cast(first.inputConsumed), 2, 512, 2.0f); + const std::vector later(reference.pcm.begin() + 512 * 2, reference.pcm.end()); + RequireDecimated(later, tail.pcm, 2, 2); + Require(std::equal(rest.begin(), rest.end(), reference.pcm.begin())); + + const auto unknown = GetResampleInfo(context, 0x3FFF); + Require(unknown.result == 4); + + for (const std::uint32_t instance : {plain, unity, faster, slower, hades, switched, resumed}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestResampleMp3(std::uint32_t context) { + std::uint32_t plain = 0; + std::uint32_t faster = 0; + std::uint32_t slower = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &plain) == 0 && sceAjmInstanceCreate(context, 0, 0, &faster) == 0 && sceAjmInstanceCreate(context, 0, 0, &slower) == 0); + const auto reference = Stream(context, plain, MP3_MONO, sizeof(MP3_MONO), 1, 1152, 0); + Require(reference.pcm.size() == 1152 * 6); + RequireDecimated(reference.pcm, Stream(context, faster, MP3_MONO, sizeof(MP3_MONO), 1, 512, 2.0f).pcm, 1, 2); + const auto interpolated = Stream(context, slower, MP3_MONO, sizeof(MP3_MONO), 1, 512, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 1); + Require(interpolated.shortJobs == 0); + for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +const std::uint8_t AT9_MONO_SILENT_SUPERFRAME[] = { + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +void TestResampleAt9(std::uint32_t context) { + const auto create = [&] { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const std::uint8_t config[8] = {0xFE, 0x70, 0x1F, 0xF0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, config, sizeof(config), result) == 0); + Submit(context, info); + Require(result[0] == 0); + return instance; + }; + std::vector stream(1024 * 3, 0); + for (std::size_t superframe = 0; superframe < 3; ++superframe) std::memcpy(stream.data() + superframe * 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const std::uint32_t plain = create(); + const std::uint32_t faster = create(); + const std::uint32_t slower = create(); + const auto reference = Stream(context, plain, stream.data(), stream.size(), 1, 256, 0); + Require(reference.pcm.size() == 3 * 4 * 256); + const auto decimated = Stream(context, faster, stream.data(), stream.size(), 1, 512, 2.0f); + RequireDecimated(reference.pcm, decimated.pcm, 1, 2); + Require(decimated.atEnd.samples == 2); + const auto interpolated = Stream(context, slower, stream.data(), stream.size(), 1, 256, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 1); + Require(interpolated.shortJobs == 0 && interpolated.afterFirstJob.samples == 256 - 256 / 2 && interpolated.atEnd.samples == 2); + for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + } int main() { @@ -692,5 +905,8 @@ int main() { TestControlAt9(context); TestControlMp3(context); TestControlOpus(context); + TestResampleOpus(context); + TestResampleMp3(context); + TestResampleAt9(context); Require(sceAjmFinalize(context) == 0); } From 23a6859a519508b0e3920e3aad20ab8adb0a660c Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Tue, 6 Oct 2026 18:07:53 -0600 Subject: [PATCH 148/203] feat(agc): declare sceAgcSetSemaphoreMemory and sceAgcDriverRegisterMultipleResources --- core/libs/prx/libSceAgc/Unimplemented.cpp | 5 +++++ core/libs/prx/libSceAgcDriver/Unimplemented.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 9c447d703..dd5e9b1f6 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -70,6 +70,11 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory() { return 0; } +int APS5_VABI sceAgcSetSemaphoreMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAgcCbMemsetExclusive() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 8add7f045..a7db84594 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -15,6 +15,11 @@ int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() { return 0; } +int APS5_VABI sceAgcDriverRegisterMultipleResources() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) { (void)owner_handle; return static_cast(0x8A6C9018); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fdb31d990..86f79503f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -31,6 +31,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info +- PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../core/libs/prx/libSceAgc/Unimplemented.cpp), [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp). Names from the shadPS4 aerolib NID list - [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters - [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list From 60e16537dc31f793e24b984fb7ceaf4adada062b Mon Sep 17 00:00:00 2001 From: raghav314 Date: Wed, 7 Oct 2026 05:41:46 +0530 Subject: [PATCH 149/203] docs: record the AJM resample assumptions in technical debt Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fdb31d990..ec5b3db53 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -161,6 +161,7 @@ Throughout the project, every function at every stage either **does exactly what - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does +- [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty ### Functional From bcdf19ee4eb9396f1a3331ea61b7f8c66256e771 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:49:35 +0200 Subject: [PATCH 150/203] perf(shader): memoize SPIR-V emission failures The source entry remembers a resource plan that failed to build, but not a failed emission: findOrCompileVariant kept only successful variants, and the first attempt used up the prepared program, so every request for a program whose emission throws ran PrepareResourceProgram and the emitter again before the draw was skipped. compileVariant now reports an exception from SpirvEmitter::Emit (and from SPIRV-Tools validation when enabled), and the source entry keeps it per code address, binding layout and resource specialization; the next request for that variant rethrows it before the disk-cache lookup. Descriptor population depends on the snapshot and an out-of-memory error is transient, so neither is kept. Nothing reaches the disk cache, and APS5_NO_FAILURE_MEMO=1 turns the memo off as for plan failures. --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 95 +++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 44 +++++++-- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 20dd3e202..bc755cfad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "ShaderCacheDirectory.hpp" +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -436,6 +438,96 @@ void verifyAcrossProcesses(const char* self) { require(std::system(command.c_str()) == 0, "the loading process failed"); } +struct ClockRequest { + std::vector code{0xf4900100u, 0x00000000u, 0xbf8cc07fu, 0x7e020204u, 0xe0700000u, 0x80000100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; + std::array capabilities{1u}; + RecompileRequest request{}; + + explicit ClockRequest(std::uint32_t stride = 0) { + userData[1] = stride << 16u; + ShaderPixelStageInfo pixel{}; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + } +}; + +std::string emissionFailure(const RecompileRequest& request) { + try { + static_cast(Recompile(request)); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + require(message.find("reads the shader clock, which needs the device's VK_KHR_shader_clock") != std::string::npos, "unexpected failure: " + message.substr(0, message.find("RecompileRequest:"))); + return message; + } + throw std::runtime_error("a shader clock read compiled without VK_KHR_shader_clock"); +} + +std::uint64_t diskMisses() { + return ShaderDiskCache::Totals().misses; +} + +void verifyEmissionFailureMemo() { + const auto before = ShaderDiskCache::Totals(); + ClockRequest clock; + const auto first = emissionFailure(clock.request); + require(diskMisses() == before.misses + 1, "the first compile was not looked up"); + require(emissionFailure(clock.request) == first, "the second compile failed with another message"); + require(diskMisses() == before.misses + 1, "the second compile looked up and emitted the program again"); + + ClockRequest layout; + layout.request.layout = {0, 0, 16, 112}; + static_cast(emissionFailure(layout.request)); + require(diskMisses() == before.misses + 2, "another binding layout reused the failure"); + ClockRequest stride(16); + static_cast(emissionFailure(stride.request)); + require(diskMisses() == before.misses + 3, "another buffer specialization reused the failure"); + ClockRequest relocated; + relocated.request.shader.codeAddress += 0x1000u; + static_cast(emissionFailure(relocated.request)); + require(diskMisses() == before.misses + 4, "a relocated copy reused the failure"); + require(emissionFailure(clock.request) == first && diskMisses() == before.misses + 4, "the other requests replaced the failure"); + + ClockRequest concurrent; + concurrent.request.layout.pushConstantSizeBytes = 64; + std::array, 4> failures; + for (auto& future : failures) future = std::async(std::launch::async, [&concurrent] { return emissionFailure(concurrent.request); }); + std::vector messages; + for (auto& future : failures) messages.push_back(future.get()); + require(std::all_of(messages.begin(), messages.end(), [&](const std::string& message) { return message == messages.front(); }), "concurrent requests failed with different messages"); + require(diskMisses() == before.misses + 5, "concurrent requests emitted the program " + std::to_string(diskMisses() - before.misses - 4) + " times"); + + ShaderDiskCache::Flush(); + require(ShaderDiskCache::Totals().writes == before.writes, "an emission failure was stored in the disk cache"); +} + +int runWithoutFailureMemo() { + ClockRequest clock; + const auto first = emissionFailure(clock.request); + require(emissionFailure(clock.request) == first, "the second compile failed with another message"); + require(diskMisses() == 2, "APS5_NO_FAILURE_MEMO=1 did not emit the program again (" + std::to_string(diskMisses()) + " lookups)"); + std::cout << "APS5_NO_FAILURE_MEMO=1 emitted the failing program twice\n"; + return 0; +} + +void verifyFailureMemoSwitch(const char* self) { + setEnvironment("APS5_NO_FAILURE_MEMO", "1"); + const std::string command = "\"" + std::string(self) + "\" --no-failure-memo"; + require(std::system(command.c_str()) == 0, "the process with APS5_NO_FAILURE_MEMO=1 failed"); +} + void verifyDefaultDirectory(const char* self) { setEnvironment("ANYPS5_SHADER_CACHE_DIR", ""); setEnvironment("ANYPS5_NO_SHADER_CACHE", "1"); @@ -452,6 +544,7 @@ void verifyDefaultDirectory(const char* self) { int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); + if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(directory); verifyDefaultDirectory(argv[0]); @@ -462,6 +555,8 @@ int main(int argc, char** argv) { verifyKeySensitivity(); verifyStore(); verifyAcrossProcesses(argv[0]); + verifyEmissionFailureMemo(); + verifyFailureMemoSwitch(argv[0]); std::error_code error; std::filesystem::remove_all(directory, error); std::cout << "shader disk cache tests passed\n"; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 817f4fc7f..d773432d0 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -9,6 +9,7 @@ #include "ShaderDiskCache.hpp" #include #include +#include #include #include #include "ControlFlow/include/ControlFlow/GraphBuilder.hpp" @@ -183,6 +184,13 @@ struct ResultMemoEntry { std::shared_ptr result; }; +struct EmissionFailure { + std::uint64_t codeAddress; + BindingLayout layout; + ResourceSpecialization specialization; + std::exception_ptr failure; +}; + struct SourceEntry { std::mutex mutex; // The code the entry was built for: the key carries only a hash of it, so a candidate entry is @@ -196,6 +204,7 @@ struct SourceEntry { std::exception_ptr planFailure; std::unique_ptr program; std::vector> variants; + std::vector emissionFailures; // The result memo, most recently used first, at most ResultMemoEntries (under mutex). std::list memo; std::unordered_map::iterator> memoIndex; @@ -225,6 +234,11 @@ struct SourceKeyHash { } }; +bool FailureMemo() { + static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr; + return memo; +} + std::shared_ptr getSource(const RecompileRequest& request) { static std::shared_mutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. @@ -266,14 +280,13 @@ std::shared_ptr getSource(const RecompileRequest& request) { { std::lock_guard lock(source->mutex); if (source->plan == nullptr) { - static const bool memoFailures = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr; - if (memoFailures && source->planFailure) std::rethrow_exception(source->planFailure); + if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); try { ResourceProgram resource(request); source->plan = std::move(resource.plan); source->program = std::move(resource.program); } catch (...) { - if (memoFailures) source->planFailure = std::current_exception(); + if (FailureMemo()) source->planFailure = std::current_exception(); throw; } } @@ -290,7 +303,7 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) { +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -320,11 +333,17 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra constexpr SpirvEmitter spirvEmitter; RecompileResult result; result.variantId = nextVariantId(); - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); - + try { + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); #if ANYPS5_ENABLE_SPIRV_TOOLS - result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets); + result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets); #endif + } catch (const std::bad_alloc&) { + throw; + } catch (...) { + if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); + throw; + } result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -383,6 +402,9 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, return candidate; } } + for (const auto& failure : source.emissionFailures) { + if (failure.codeAddress == request.shader.codeAddress && sameLayout(failure.layout, request.layout) && failure.specialization == specialization) std::rethrow_exception(failure.failure); + } cacheHit = false; const bool disk = ShaderDiskCache::Enabled() && !DebugProbeActive(); std::vector diskKey; @@ -400,7 +422,13 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, if (variant == nullptr) { auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request); source.program.reset(); - variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization)); + std::exception_ptr emissionFailure; + try { + variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization, &emissionFailure)); + } catch (...) { + if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, specialization, emissionFailure}); + throw; + } if (disk) ShaderDiskCache::Store(std::move(diskKey), variant); } source.program.reset(); From 54eeadb3a2cd5e467bc892bb82d0a3675ba5b6af Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 22:34:04 -0300 Subject: [PATCH 151/203] feat(libSceAgc): implement sceAgcWaitRegMemPatchMask Patch the mask of a wait packet like sceAgcWaitRegMemPatchReference patches its reference: a 32-bit value written to the low mask word of a 32- or 64-bit WAIT_REG_MEM, rejected above 32 bits. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp | 7 +++++++ core/libs/prx/libSceAgc/tests/Commands.cpp | 10 ++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 18 insertions(+) diff --git a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp index daa5b4216..27bfd5b3d 100644 --- a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp @@ -29,6 +29,13 @@ int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t r return 0; } +int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) { + auto* wait = Agc::Command::ValidateWait(cmd, __func__); + Agc::Command::CheckBits(mask, 0xffffffffu, __func__); + wait[((wait[0] >> 8u) & 0xffu) == 0x3cu ? 5 : 6] = static_cast(mask); + return 0; +} + int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); Agc::Command::Require(compareFunction <= 6, __func__, "invalid wait comparison"); diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 592be7022..da6a40a28 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -19,6 +19,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, s extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier); extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask); extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type); extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues); extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, std::uint32_t operation); @@ -334,6 +335,15 @@ void testMemory() { check(storage.words == before, "invalid memory operation modified packet memory"); auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__); check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask"); + sceAgcWaitRegMemPatchMask(packet, 0x0f0f0f0fu); + check(packet[10] == 0x0f0f0f0fu && packet[11] == 0xffffffffu && packet[8] == 7, "64-bit mask patch changed the wrong word"); + sceAgcWaitRegMemPatchMask(truncated, 0xff00u); + check(truncated[9] == 0xff00u && truncated[8] == 0u && truncated[10] == 2u, "32-bit mask patch changed the wrong word"); + const auto beforeMask = storage.words; + expectFailure([&] { sceAgcWaitRegMemPatchMask(packet, 0x100000000ull); }); + expectFailure([&] { sceAgcWaitRegMemPatchMask(truncated, 0x100000000ull); }); + expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); }); + check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } void testDefaults() { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fdb31d990..79fb39377 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -32,6 +32,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [sceAgcWaitRegMemPatchMask](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws - [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown From 8ab5fab84aec3ab508755183237136783f937a2a Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 22:52:27 -0300 Subject: [PATCH 152/203] feat(libSceAgc): implement sceAgcDcbSetCfRegisterDirect and RangeDirect Write SET_CONFIG_REG (0x68) packets like the context, shader and user config register setters: one register from a ShaderRegister, or a range of values at an offset. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgc/DcbState/src/Registers.cpp | 8 ++++++ .../prx/libSceAgc/tests/RegisterCommands.cpp | 26 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 35 insertions(+) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp index 11d3ce528..f9b0b4c99 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp @@ -8,6 +8,14 @@ extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer* buf, ShaderRegister reg) { + return Agc::Command::WriteRegisterRange(buf, 0x68u, reg.offset, ®.value, 1u, __func__); +} + +std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues) { + return Agc::Command::WriteRegisterRange(buf, 0x68u, offset, values, numValues, __func__); +} + uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer* buf, ShaderRegister reg) { return Agc::Command::WriteRegisterRange(buf, 0x69u, reg.offset, ®.value, 1u, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index ffe1e8690..5c9f151ed 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -6,6 +6,8 @@ #include extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister); +std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister); @@ -69,6 +71,29 @@ void testDirect() { } } +void testConfig() { + Storage storage; + storage.words.fill(0xabcdef01u); + auto* direct = sceAgcDcbSetCfRegisterDirect(&storage.buffer, {0x2468u, 0x12345678u}); + const std::array expectedDirect{0xc0016800u, 0x2468u, 0x12345678u}; + check(direct == storage.words.data() && std::equal(expectedDirect.begin(), expectedDirect.end(), direct), "incorrect config register packet"); + const std::array values{1, 2, 3}; + auto* range = sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), values.size()); + const std::array expectedRange{0xc0036800u, 0x100u, 1u, 2u, 3u}; + check(range == direct + 3 && std::equal(expectedRange.begin(), expectedRange.end(), range), "incorrect config register range packet"); + auto* reserved = sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x200u, nullptr, 2); + check(reserved == range + 5 && reserved[0] == 0xc0026800u && reserved[1] == 0x200u && reserved[2] == 0xabcdef01u && reserved[3] == 0xabcdef01u, "null config values wrote the payload"); + check(storage.buffer.cursor_up == reserved + 4 && reserved[4] == 0xabcdef01u, "config register cursor mismatch"); + const auto before = storage.words; + expectFailure([&] { sceAgcDcbSetCfRegisterDirect(&storage.buffer, {0x10000u, 1}); }); + expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); }); + expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); }); + expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); }); + const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); + expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); }); + check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer"); +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -157,6 +182,7 @@ void testDirectList() { int main() { try { testDirect(); + testConfig(); testIndirect(); testDirectList(); std::puts("AGC register command tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ec5b3db53..5a16bc7ed 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -33,6 +33,7 @@ Throughout the project, every function at every stage either **does exactly what - [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters - [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted From 425ed24795af3f0d0c3214c7b72b9feb772fe1ac Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 23:05:18 -0300 Subject: [PATCH 153/203] feat(libSceAgc): implement sceAgcDcbClearState Write the 2-dword CLEAR_STATE (0x12) packet with a 4-bit command, the packet the driver already validates and executes by restoring the initial context registers. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp | 5 +++++ core/libs/prx/libSceAgc/tests/Commands.cpp | 17 +++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 23 insertions(+) diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp index 7dc690c4a..bb68d80d8 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp @@ -34,6 +34,11 @@ std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t o return Agc::Command::Emit(buf, 0x12u, {state}, __func__); } +std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command) { + Agc::Command::CheckBits(command, 0xfu, __func__); + return Agc::Command::Emit(buf, 0x12u, {command}, __func__); +} + uint32_t* APS5_VABI sceAgcDcbRewind(CommandBuffer* buf, uint32_t initial_state) { return Agc::Command::WriteRewind(buf, initial_state, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 592be7022..c6760be18 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -11,6 +11,7 @@ #include extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" int APS5_VABI sceAgcSuspendPoint(); extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); @@ -98,6 +99,21 @@ void testPackets() { check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed allocation advanced cursor"); } +void testClearState() { + Storage storage; + storage.words.fill(0xdeadbeefu); + for (std::uint32_t command = 0; command <= 0xfu; ++command) { + auto* packet = sceAgcDcbClearState(&storage.buffer, command); + check(packet == storage.words.data() + command * 2 && packet[0] == 0xc0001200u && packet[1] == command, "incorrect CLEAR_STATE packet"); + } + check(storage.buffer.cursor_up == storage.words.data() + 32 && storage.words[32] == 0xdeadbeefu, "incorrect CLEAR_STATE cursor advance"); + const auto before = storage.words; + expectFailure([&] { sceAgcDcbClearState(&storage.buffer, 0x10u); }); + expectFailure([&] { sceAgcDcbClearState(&storage.buffer, 0xffffffffu); }); + expectFailure([] { sceAgcDcbClearState(nullptr, 0); }); + check(storage.words == before && storage.buffer.cursor_up == storage.words.data() + 32, "invalid CLEAR_STATE modified the buffer"); +} + struct ContextGrowth { Storage destination; std::uint32_t* expectedCursor; @@ -366,6 +382,7 @@ int main(int argc, char** argv) { return 0; } testPackets(); + testClearState(); testIndexedIndirectDraws(); testMarkers(); testIndexBuffer(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 23d063145..5ed66791d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -36,6 +36,7 @@ Throughout the project, every function at every stage either **does exactly what - [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown +- [sceAgcDcbClearState](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - the (buffer, command) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs) and [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c), and the 2-dword CLEAR_STATE (0x12) packet is OpenAGC's; OpenAGC masks the command to 4 bits, here a command above 0xf throws, as the driver rejects that payload - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown From 4e18def4c166e012172cd1460f8563e9f2a4069f Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:24:35 +0200 Subject: [PATCH 154/203] fix(agc): accept unaligned shader headers Driver::RegisterShader required the Shader header at an 8-byte boundary (alignof(Shader)) and read its fields through the pointer. PPSA26344 uses its shader packs in place: its compute shader objects point at headers 4 and 1 bytes past an 8-byte boundary, so the first sceAgcCreateShader of such a header threw "null or misaligned address". The header is now checked for null and readable memory at any address, and its fixed fields and the registers the trace prints are copied out with memcpy. The code address must still be 256-byte aligned, and the header and size checks are unchanged. The console's rule is unknown and is listed in TechnicalDebt. --- .../src/Driver/Shaders/ShaderRegistry.cpp | 39 ++++++++++++------- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 31 +++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 8ba86476a..85acc59f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -163,25 +163,34 @@ std::uint64_t NullPixelProgramAddress() { void Driver::RegisterShader(const Shader* shader) { CheckFailure(); - GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader)); - require(shader->file_header == 0x34333231u && shader->version == 0x18u, "invalid shader header"); - require(shader->header_size >= sizeof(Shader), "shader header is smaller than its fixed fields"); - require(shader->shader_size != 0 && (shader->shader_size & 3u) == 0, "invalid shader size"); - GuestMemory::CheckRange(shader, shader->header_size, alignof(Shader)); - const auto* code = const_cast(shader->code); - GuestMemory::CheckRange(code, shader->shader_size, 256); - ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(shader), shader->type, {}, {}}; - snapshot.code.resize(shader->shader_size / sizeof(std::uint32_t)); - std::memcpy(snapshot.code.data(), code, shader->shader_size); - snapshot.header.resize(shader->header_size); - std::memcpy(snapshot.header.data(), shader, shader->header_size); + GuestMemory::CheckRange(shader, sizeof(Shader), 1); + Shader fields; + std::memcpy(&fields, static_cast(shader), sizeof(Shader)); + require(fields.file_header == 0x34333231u && fields.version == 0x18u, "invalid shader header"); + require(fields.header_size >= sizeof(Shader), "shader header is smaller than its fixed fields"); + require(fields.shader_size != 0 && (fields.shader_size & 3u) == 0, "invalid shader size"); + GuestMemory::CheckRange(shader, fields.header_size, 1); + const auto* code = const_cast(fields.code); + GuestMemory::CheckRange(code, fields.shader_size, 256); + ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(shader), fields.type, {}, {}}; + snapshot.code.resize(fields.shader_size / sizeof(std::uint32_t)); + std::memcpy(snapshot.code.data(), code, fields.shader_size); + snapshot.header.resize(fields.header_size); + std::memcpy(snapshot.header.data(), static_cast(shader), fields.header_size); static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS"); if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) { - std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast(snapshot.codeAddress), shader->type); - for (std::uint32_t i = 0; i < shader->num_cx_registers && shader->cx_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->cx_registers[i].offset, shader->cx_registers[i].value); + const auto print = [](const ShaderRegister* registers, std::uint32_t count) { + for (std::uint32_t i = 0; i < count && registers != nullptr; ++i) { + ShaderRegister value; + std::memcpy(&value, static_cast(registers + i), sizeof(value)); + std::fprintf(stderr, " %x=%08x", value.offset, value.value); + } + }; + std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast(snapshot.codeAddress), fields.type); + print(fields.cx_registers, fields.num_cx_registers); std::fprintf(stderr, " sh"); - for (std::uint32_t i = 0; i < shader->num_sh_registers && shader->sh_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->sh_registers[i].offset, shader->sh_registers[i].value); + print(fields.sh_registers, fields.num_sh_registers); std::fprintf(stderr, "\n"); } std::lock_guard lock(mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 1e7f5d2a1..d611db473 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -374,6 +376,34 @@ void testMultiSubmissions() { } } +void testShaderHeaderAlignment() { + alignas(256) static const std::array code{0xbf810000}; + alignas(8) static std::array storage{}; + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(code); + shader.code = code.data(); + const auto at = [](std::size_t offset, const Shader& fields) { + std::memcpy(storage.data() + offset, &fields, sizeof(fields)); + return reinterpret_cast(storage.data() + offset); + }; + for (const std::size_t offset : {0, 4, 1}) AgcDriverRegisterShader_nid_postfix(at(offset, shader)); + const auto refused = [](const std::string& message, const char* reason) { check(message.find(reason) != std::string::npos, message.c_str()); }; + refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(nullptr); }), "null or misaligned address"); + refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(reinterpret_cast(0x1001)); }), "not readable"); + Shader misplaced = shader; + misplaced.code = code.data() + 1; + refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, misplaced)); }), "null or misaligned address"); + Shader older = shader; + older.version = 0x17; + refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(1, older)); }), "invalid shader header"); + Shader truncated = shader; + truncated.header_size = sizeof(Shader) - 4; + refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, truncated)); }), "smaller than its fixed fields"); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -470,6 +500,7 @@ int main() { testWaitFreeSubmissionBehindHeldOne(); testWaitFreeSubmissionTheCpuWaitsFor(); testMultiSubmissions(); + testShaderHeaderAlignment(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC driver submit tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dbd9ee2f3..dd9d86092 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -165,6 +165,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty From 5bae4df0b6eead1972f78124029beb9183046b0c Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Tue, 6 Oct 2026 22:20:51 -0400 Subject: [PATCH 155/203] feat(libSceNgs2.native): mix a stereo mastering voice into a 6 or 8 channel render buffer The mixing stage threw when a mastering voice and its render buffer had different channel counts. A 2-channel voice is now mixed into the first two channels of a 6- or 8-channel buffer; other mismatches still throw. --- .../libs/prx/libSceNgs2.native/src/Render.cpp | 9 ++- .../libSceNgs2.native/tests/Ngs2Render.cpp | 80 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 8f307efb8..1e81d0d6b 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -188,12 +188,16 @@ static std::size_t SampleBytes(std::uint32_t waveformType) { throw std::runtime_error("NGS2: render waveform type " + Ngs2Hex(waveformType) + " is not implemented"); } +static bool IsStereoIntoSurround(const Ngs2Voice& voice, const Ngs2RenderBufferInfo& output) { + return voice.channels == 2 && (output.num_channels == 6 || output.num_channels == 8); +} + static std::vector& OutputMix(std::vector>& mixes, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo, const Ngs2Voice& voice, std::uint32_t grain) { if (voice.outputId >= numBufferInfo) throw std::invalid_argument("NGS2: mastering output " + std::to_string(voice.outputId) + " has no render buffer"); const auto& output = bufferInfo[voice.outputId]; const auto sampleBytes = SampleBytes(output.waveform_type); - if (output.num_channels != voice.channels) throw std::runtime_error("NGS2: mixing " + std::to_string(voice.channels) + " mastering channels into " + std::to_string(output.num_channels) + " is not implemented"); + if (output.num_channels != voice.channels && !IsStereoIntoSurround(voice, output)) throw std::runtime_error("NGS2: mixing " + std::to_string(voice.channels) + " mastering channels into " + std::to_string(output.num_channels) + " is not implemented"); if (output.buffer == nullptr || output.buffer_size < static_cast(grain) * output.num_channels * sampleBytes) { throw std::invalid_argument("NGS2: render buffer " + std::to_string(voice.outputId) + " is missing or too small"); } @@ -227,10 +231,11 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo RenderVoice(*voice, voices, grain, system.option.sample_rate); if (voice->rack->rackId != SCE_NGS2_RACK_ID_MASTERING || !voice->hasSamples) continue; auto& mix = OutputMix(mixes, bufferInfo, numBufferInfo, *voice, grain); + const std::uint32_t outputChannels = bufferInfo[voice->outputId].num_channels; for (std::uint32_t channel = 0; channel < voice->channels; channel++) { const bool lfe = channel == 3 && (voice->channels == 6 || voice->channels == 8); const float level = lfe ? voice->lfeLevel : voice->fbwLevel; - for (std::uint32_t i = 0; i < grain; i++) mix[i * voice->channels + channel] += voice->samples[channel * grain + i] * level; + for (std::uint32_t i = 0; i < grain; i++) mix[i * outputChannels + channel] += voice->samples[channel * grain + i] * level; } } for (std::uint32_t i = 0; i < numBufferInfo; i++) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 3aa1d2cb3..c26310763 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -322,6 +322,85 @@ static void TestMasteringGain() { Require(sceNgs2SystemDestroy(loudSystem, nullptr) == SCE_NGS2_OK); } +static uintptr_t RackVoice(uintptr_t rack, std::uint32_t index) { + uintptr_t voice = 0; + Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK && voice != 0); + return voice; +} + +static uintptr_t MasteringRack(uintptr_t system, std::uint32_t voices) { + Ngs2MasteringRackOption option{}; + option.rack_option.size = sizeof(option); + option.rack_option.max_grain_samples = 512; + option.rack_option.max_voices = voices; + option.rack_option.max_input_delay_blocks = 1; + option.rack_option.max_matrices = 1; + option.rack_option.max_ports = 8; + option.max_channels = 8; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_MASTERING, &option.rack_option, &query) == SCE_NGS2_OK); + const auto buffer = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_MASTERING, &option.rack_option, &buffer, &rack) == SCE_NGS2_OK && rack != 0); + return rack; +} + +static void StereoSource(uintptr_t voice, const std::vector& pcm, uintptr_t master) { + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + const float levels[2] = {1.0f, 0.5f}; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 2, levels}); + Control(voice, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(voice, master); +} + +static void TestStereoIntoSurround() { + const auto system = CreateSystem(); + const auto masteringRack = MasteringRack(system, 3); + const auto samplerRack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + uintptr_t masters[3]; + for (std::uint32_t i = 0; i < 3; i++) { + masters[i] = RackVoice(masteringRack, i); + Control(masters[i], SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 2, 0}); + Control(masters[i], SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, i, 0}); + Event(masters[i], SCE_NGS2_VOICE_EVENT_PLAY); + } + const std::vector pcm(Grain, 16384); + for (std::uint32_t i = 0; i < 2; i++) { + const auto source = RackVoice(samplerRack, i); + StereoSource(source, pcm, masters[i]); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + } + std::vector outFive(Grain * 6, -1.0f); + std::vector outSeven(Grain * 8, -1.0f); + std::vector outQuad(Grain * 4, -1.0f); + const Ngs2RenderBufferInfo info[3] = { + {outFive.data(), outFive.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 6}, + {outSeven.data(), outSeven.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 8}, + {outQuad.data(), outQuad.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4}, + }; + Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(outFive[i * 6] == 0.5f && outFive[i * 6 + 1] == 0.25f); + for (std::uint32_t channel = 2; channel < 6; channel++) Require(outFive[i * 6 + channel] == 0.0f); + Require(outSeven[i * 8] == 0.5f && outSeven[i * 8 + 1] == 0.25f); + for (std::uint32_t channel = 2; channel < 8; channel++) Require(outSeven[i * 8 + channel] == 0.0f); + } + + const auto rejectedSource = RackVoice(samplerRack, 2); + StereoSource(rejectedSource, pcm, masters[2]); + Event(rejectedSource, SCE_NGS2_VOICE_EVENT_PLAY); + bool threw = false; + try { + sceNgs2SystemRender(system, info, 3); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestLock() { const auto system = CreateSystem(); Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); @@ -366,6 +445,7 @@ int main() { TestSampleRate(); TestUserData(); TestMasteringGain(); + TestStereoIntoSurround(); TestLock(); TestAllocator(); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dbd9ee2f3..b79bfb688 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -120,6 +120,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp), and the Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation, have unknown signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [NGS2 mastering voice gain](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 +- [NGS2 stereo mastering voice into a surround buffer](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info From f7d4fbab9a3b597aae9817cd472313649ce5225f Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:22:23 +0200 Subject: [PATCH 156/203] perf(shader): share the BDA byte read fallback between read sites A BDA dword read whose span the probe rejects falls back to four byte lookups with a branch each, and every fault was recorded inline with a compare-exchange and seven stores. Both were copied into every read site and both wave64 lane halves, so one large compute program grows to about a million SPIR-V words and spends about 95 s in vkCreateComputePipelines. Faults are now recorded by one DontInline function, record_bda_fault, and the byte fallback of a dword is read_bda_dword_bytes, in a stop and a coherent variant, returning the value and whether the invocation stops. Read order, fault reasons, pcs, Volatile and the stop behaviour are unchanged, and so is the probe path. --- .../libSceAgcDriver/tests/BdaExecution.cpp | 33 +++++++++ .../prx/libSceAgcDriver/tests/BdaShader.cpp | 46 ++++++++++-- .../prx/libSceAgcDriver/tests/BdaShader.hpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 74 +++++++++++++++++++ .../include/SpirvBackend/SpirvBda.hpp | 2 + .../SpirvBackend/SpirvEmitterState.hpp | 3 + .../SpirvBackend/src/SpirvBdaFault.cpp | 32 +++++++- .../SpirvBackend/src/SpirvBdaLookup.cpp | 2 + .../SpirvBackend/src/SpirvBdaRead.cpp | 59 ++++++++++++++- 9 files changed, 240 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 4cea9514c..faf2f911c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -130,6 +130,39 @@ void RunBdaExecutionTests(const Context& context) { run(guest, 8, 0, Abi::FaultReason::InvalidTable, 3); run(std::numeric_limits::max() - 2, 8, 0, Abi::FaultReason::Overflow, 2, 1, 4); run(1, 8, 0, Abi::FaultReason::Overflow, 2, 1, -4); + Buffer words(context, 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto read = [&](std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops, std::uint32_t value, bool faults) { + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + const std::array sentinel{0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + std::memcpy(words.Bytes().data(), sentinel.data(), sizeof(sentinel)); + Pipeline pipeline(context, MakeBdaDwordReadTestShader(address, dwords, coherent, stops), {&table, &fault, &words}); + pipeline.Run(1); + Abi::Fault report{}; + std::array result{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + std::memcpy(result.data(), words.Bytes().data(), sizeof(result)); + auto expected = sentinel; + if (!faults || !stops) { + expected[0] = value; + for (std::uint32_t dword = 1; dword < dwords; ++dword) expected[dword] = 0; + } + if (faults) { + Require(report.state == Abi::FaultState::Ready && report.reason == Abi::FaultReason::Unmapped && report.address == guest + 5 && report.bytes == 1 && report.instruction == 0x1234, "BDA dword read did not publish its first fault"); + } else { + Require(report.state == Abi::FaultState::Empty, "mapped BDA dword read published a fault"); + } + Require(result == expected, faults && stops ? "faulting BDA dword read continued to output a substitute value" : "BDA dword read produced incorrect data"); + }; + for (const bool stops : {true, false}) { + for (const bool coherent : {false, true}) { + read(guest + 1, 1, coherent, stops, 0x55443322, false); + read(guest + 3, 1, coherent, stops, 0x5544, true); + read(guest + 1, 4, coherent, stops, 0x55443322, true); + } + } ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index b05dabe90..9c4dd099d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -3,11 +3,19 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" -std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset) { +namespace { + +template +std::vector MakeShader(std::uint64_t address, bool coherent, bool stops, const TEmit& emit) { using namespace ShaderRecompiler; IrProgram program; program.Resources().stage = IrShaderStage::Compute; program.Info().usesDma = true; + if (coherent) { + MemoryInfo memory; + memory.coherent = true; + program.Resources().memoryInfo = {memory}; + } SpirvEmitterState state(program, {}); EmitBaseHeader(state.module, program); const auto define = [&](std::uint32_t binding) { @@ -19,6 +27,12 @@ std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_ state.bdaPagetableVariable = define(0); state.faultBufferVariable = define(1); const auto output = define(2); + std::vector interfaceVariables; + if (!stops) { + interfaceVariables.push_back(state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput)); + state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex); + } + state.bdaStopsInvocations = stops; DefineGetBdaPointer(state); const auto main = state.module.AllocateId(); state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); @@ -29,12 +43,34 @@ std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_ flags.pc = 0x1234; instruction.SetFlags(flags); auto base = BdaConstant(state, address); - if (offset != 0) base = AddBdaAddress(ctx, instruction, base, BdaConstant(state, offset < 0 ? std::uint64_t{0} - static_cast(offset) : static_cast(offset)), offset < 0); - const auto value = EmitBdaRead(ctx, instruction, base, bits); - state.module.AddFunction(spv::OpStore, BdaWord(state, output, ConstantU32(state, 0u)), value); + if (!stops) { + const auto index = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), index, interfaceVariables[0]); + base = Binary(state, spv::OpIAdd, TypeScalarU64(state), base, Unary(state, spv::OpUConvert, TypeScalarU64(state), index)); + } + const auto values = emit(ctx, instruction, base); + for (std::uint32_t index = 0; index < values.size(); ++index) state.module.AddFunction(spv::OpStore, BdaWord(state, output, ConstantU32(state, index)), values[index]); state.module.AddFunction(spv::OpReturn); state.module.AddFunction(spv::OpFunctionEnd); state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, 1u, 1u, 1u); - state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", {}); + state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables); return state.module.Finalize(); } + +} + +std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset) { + using namespace ShaderRecompiler; + return MakeShader(address, false, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + if (offset != 0) base = AddBdaAddress(ctx, instruction, base, BdaConstant(ctx.state, offset < 0 ? std::uint64_t{0} - static_cast(offset) : static_cast(offset)), offset < 0); + return std::vector{EmitBdaRead(ctx, instruction, base, bits)}; + }); +} + +std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops) { + using namespace ShaderRecompiler; + return MakeShader(address, coherent, stops, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + const auto values = EmitBdaDwordReads(ctx, instruction, base, 0u, dwords, true); + return std::vector(values.begin(), values.begin() + dwords); + }); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 312a64ba1..2baa4468a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -6,6 +6,7 @@ #include std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0); +std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a65c7d9e5..723598c00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -11,6 +11,7 @@ #include "SpirvBackend/SpirvOptimizer.hpp" #endif #include "CacheKey.hpp" +#include "BdaAbi.hpp" #include #include #include @@ -893,6 +894,78 @@ void verifyTwoLaneUniformValues() { require(multiplies(32u) == oneLane, "two-lane uniform values: a two-lane invocation computes a scalar value once per lane"); } +void verifyBdaReadFallbackFunctions() { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + alignas(256) static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; + const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent) { + std::vector code{0x7e020200u, 0x7e040201u}; + for (std::uint32_t load = 0; load < loads; ++load) { + code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (4u * load + 4u)); + code.push_back(((3u + load) << 24u) | 0x007d0001u); + } + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 1; load < loads; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u)); + if (barrier) code.push_back(0xbf8a0000u); + code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x50000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + for (const bool barrier : {false, true}) { + for (const bool coherent : {false, true}) { + const auto one = compile(1u, barrier, coherent); + const auto five = compile(5u, barrier, coherent); + std::map names; + std::map definitions; + const auto reader = std::string("read_bda_dword_bytes") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); + std::string function; + std::size_t compareExchanges = 0; + std::size_t mainLookups = 0; + std::array readerLoads{}; + for (std::size_t cursor = 5; cursor < five.size();) { + const auto length = five[cursor] >> 16u; + require(length != 0 && length <= five.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); + const auto op = five[cursor] & 0xffffu; + if (op == spv::OpName) names[five[cursor + 1]] = reinterpret_cast(&five[cursor + 2]); + if (op == spv::OpFunction) { + function = names[five[cursor + 2]]; + if (function == "record_bda_fault" || function.starts_with("read_bda_dword_bytes")) { + require((five[cursor + 3] & spv::FunctionControlDontInlineMask) != 0u, "BDA read functions: a fault or byte read function may be inlined"); + ++definitions[function]; + } + } + if (op == spv::OpAtomicCompareExchange) ++compareExchanges; + if (op == spv::OpFunctionCall && function == "main" && names[five[cursor + 3]] == "get_bda_pointer") ++mainLookups; + if (op == spv::OpLoad && function == reader) ++readerLoads[length > 4u && (five[cursor + 4] & spv::MemoryAccessVolatileMask) != 0u]; + cursor += length; + } + require(definitions["record_bda_fault"] == 1u && definitions[reader] == 1u, "BDA read functions: the fault and byte read functions are not defined"); + for (const auto& [name, count] : definitions) require(count == 1u, "BDA read functions: a function is defined twice"); + require(compareExchanges == 1u, "BDA read functions: a fault is recorded outside record_bda_fault"); + require(mainLookups == 0u, "BDA read functions: a read site looks up its bytes inline"); + require(readerLoads[coherent] == 4u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(five.size() - one.size() < 4u * 300u, "BDA read functions: a read site takes 300 SPIR-V words or more"); + } + } +} + void verifyFunctionLdsBound() { using namespace ShaderRecompiler; const auto build = [](const auto& body) { @@ -1048,6 +1121,7 @@ int main() { verifyComputedTexelOffsets(); verifyWaveUniformValues(); verifyTwoLaneUniformValues(); + verifyBdaReadFallbackFunctions(); verifyFunctionLdsBound(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index b06ea3e98..a1323899b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -13,6 +13,8 @@ std::uint32_t BdaConstant(SpirvEmitterState& state, std::uint64_t value); std::uint32_t BdaWord(SpirvEmitterState& state, std::uint32_t variable, std::uint32_t index); std::uint32_t BdaLoadWord(SpirvEmitterState& state, std::uint32_t index); std::uint32_t BdaLoadAddress(SpirvEmitterState& state, std::uint32_t index); +void DefineBdaFaultFunction(SpirvEmitterState& state); +void DefineBdaDwordReadFunctions(SpirvEmitterState& state); void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1e761cf73..9b6794e0d 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -105,6 +105,9 @@ struct SpirvEmitterState { std::uint32_t bdaProbeFunction = 0; std::uint32_t bdaWritePointerFunction = 0; std::uint32_t bdaNoteWriteFunction = 0; + std::uint32_t bdaFaultFunction = 0; + std::array, 2> bdaDwordReadFunctions {}; + std::uint32_t bdaStopValue = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). bool bdaStopsInvocations = true; // Execution scope of the barriers that keep one guest wave's LDS accesses in program order across diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 711619793..ad34e63e2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include namespace ShaderRecompiler { @@ -26,7 +27,21 @@ std::uint32_t BdaLoadAddress(SpirvEmitterState& state, std::uint32_t index) { return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, BdaConstant(state, 32u))); } -void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { +void DefineBdaFaultFunction(SpirvEmitterState& state) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + state.bdaFaultFunction = state.module.AllocateId(); + state.module.AddName(state.bdaFaultFunction, "record_bda_fault"); + state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.bdaFaultFunction, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, TypeVoid(state), u32, u64, u32, u32)); + const auto reason = state.module.AllocateId(); + const auto address = state.module.AllocateId(); + const auto bytes = state.module.AllocateId(); + const auto instruction = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, reason); + state.module.AddFunction(spv::OpFunctionParameter, u64, address); + state.module.AddFunction(spv::OpFunctionParameter, u32, bytes); + state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); + EmitLabel(state, state.module.AllocateId()); const auto pointer = BdaWord(state, state.faultBufferVariable, ConstantU32(state, 0u)); const auto previous = state.module.AllocateId(); const auto scope = ConstantU32(state, spv::ScopeDevice); @@ -37,7 +52,7 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 const auto store = [&](std::uint32_t index, std::uint32_t value) { state.module.AddFunction(spv::OpStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, index)), value); }; - store(1, ConstantU32(state, static_cast(reason))); + store(1, reason); store(2, Unary(state, spv::OpUConvert, TypeU32(state), address)); store(3, Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, 32u)))); store(4, bytes); @@ -47,6 +62,13 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 const auto release = ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask); state.module.AddFunction(spv::OpAtomicStore, pointer, scope, release, ConstantU32(state, static_cast(BdaAbi::FaultState::Ready))); }); + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); +} + +void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { + if (state.bdaFaultFunction == 0) throw std::runtime_error("BDA fault function is missing"); + state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast(reason)), address, bytes, instruction); } void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { @@ -67,7 +89,11 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) { state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); EmitLabel(state, failed); - state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); + if (state.bdaStopValue != 0) { + state.module.AddFunction(spv::OpReturnValue, state.bdaStopValue); + } else { + state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); + } EmitLabel(state, next); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index f3ef2e128..98c1b24d2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -158,8 +158,10 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { void DefineGetBdaPointer(SpirvEmitterState& state) { if (!state.program.Info().usesDma) return; + DefineBdaFaultFunction(state); state.bdaPointerFunction = DefineBdaLookup(state, "get_bda_pointer", true); if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); + DefineBdaDwordReadFunctions(state); if (state.program.Info().bdaWrites) { state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); state.bdaNoteWriteFunction = DefineBdaNoteWrite(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index df12bfd78..db3fb5db8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace ShaderRecompiler { @@ -16,10 +17,18 @@ void EmitBdaOverflowCheck(SpirvEmitterState& state, std::uint32_t address, std:: StopBdaInvocationIf(state, overflow); } -std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { +bool BdaCoherent(const SpirvEmitterState& state, const IrValue& inst) { const auto index = inst.Flags().index; const auto& memory = state.program.Resources().memoryInfo; - return spv::MemoryAccessAlignedMask | (index < memory.size() && memory[index].coherent ? spv::MemoryAccessVolatileMask : 0u); + return index < memory.size() && memory[index].coherent; +} + +std::uint32_t BdaAccessMask(bool coherent) { + return spv::MemoryAccessAlignedMask | (coherent ? spv::MemoryAccessVolatileMask : 0u); +} + +std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { + return BdaAccessMask(BdaCoherent(state, inst)); } std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::uint32_t instruction, std::uint32_t accessMask) { @@ -100,6 +109,40 @@ std::uint32_t BdaAddressUnaligned(SpirvEmitterState& state, std::uint32_t addres } +void DefineBdaDwordReadFunctions(SpirvEmitterState& state) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto pair = TypeU32Vector(state, 2); + const auto type = state.module.Type(spv::OpTypeFunction, pair, u64, u32); + const auto& memory = state.program.Resources().memoryInfo; + const bool coherentAccesses = std::any_of(memory.begin(), memory.end(), [](const MemoryInfo& info) { return info.coherent; }); + const bool stops = state.bdaStopsInvocations; + if (stops) state.bdaStopValue = state.module.Constant(spv::OpConstantComposite, pair, ConstantU32(state, 0u), ConstantU32(state, 1u)); + for (const bool stop : {false, true}) { + for (const bool coherent : {false, true}) { + if ((stop && !stops) || (coherent && !coherentAccesses)) continue; + const auto function = state.module.AllocateId(); + state.module.AddName(function, std::string("read_bda_dword_bytes") + (stop ? "_stop" : "") + (coherent ? "_coherent" : "")); + state.module.AddFunction(spv::OpFunction, pair, function, spv::FunctionControlDontInlineMask, type); + const auto address = state.module.AllocateId(); + const auto instruction = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u64, address); + state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); + EmitLabel(state, state.module.AllocateId()); + state.bdaStopsInvocations = stop; + EmitBdaOverflowCheck(state, address, 4u, instruction); + const auto value = EmitBdaBytes(state, address, 4u, instruction, BdaAccessMask(coherent)); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, pair, result, value, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpReturnValue, result); + state.module.AddFunction(spv::OpFunctionEnd); + state.bdaDwordReadFunctions[stop][coherent] = function; + } + } + state.bdaStopsInvocations = stops; + state.bdaStopValue = 0; +} + // APS5_BDA_BYTE_READS=1 restores one lookup and one byte load per byte for every read. bool BdaByteReadsForced() { static const bool forced = std::getenv("APS5_BDA_BYTE_READS") != nullptr; @@ -243,11 +286,19 @@ std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const std::array addresses{}; const auto dwordAddress = [&](std::uint32_t dword) { return AddBdaImmediate(ctx, inst, address, static_cast(offset + dword * 4u)); }; const auto readBytes = [&](std::array& into) { + const auto function = state.bdaDwordReadFunctions[state.bdaStopsInvocations][BdaCoherent(state, inst)]; + if (function == 0) ctx.Fail(inst, "BDA dword read function is missing"); for (std::uint32_t dword = first; dword <= last; ++dword) { if (!isUsed(dword)) continue; if (dword != first) addresses[dword] = dwordAddress(dword); - EmitBdaOverflowCheck(state, addresses[dword], 4u, instruction); - into[dword] = EmitBdaBytes(state, addresses[dword], 4u, instruction, BdaAccessMask(state, inst)); + const auto read = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), read, function, addresses[dword], instruction); + into[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), into[dword], read, 0u); + if (!state.bdaStopsInvocations) continue; + const auto stopped = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stopped, read, 1u); + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, TypeBool(state), stopped, ConstantU32(state, 0u))); } }; addresses[first] = dwordAddress(first); From f5a6a31b61334605d89255bdee09762d95d8f8c2 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Wed, 7 Oct 2026 08:09:54 +0530 Subject: [PATCH 157/203] fix(agc): read unwritten user data registers as zero A stage's user data registers are read for the count in its RSRC2 (COMPUTE_PGM_RSRC2 for compute). A register the title never wrote no longer refuses the draw or dispatch: it reads 0, as Kyty and SharpEmu read it and as AMD PAL and Mesa initialize the user data registers. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Driver/Diagnostics.hpp | 2 ++ .../Execution/src/Driver/Diagnostics.cpp | 5 +++++ .../Execution/src/Driver/Dispatch/Dispatch.cpp | 2 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 10 ++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index cfb690a7b..3c0093fbe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -12,6 +12,8 @@ void require(bool condition, const char* reason); std::uint32_t readRegister(const Registers& registers, std::uint32_t offset); +std::uint32_t readUserData(const Registers& shader, std::uint32_t offset); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 002fb2ed3..59d038112 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -21,4 +21,9 @@ std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) { return it->second; } +std::uint32_t readUserData(const Registers& shader, std::uint32_t offset) { + const auto it = shader.find(offset); + return it == shader.end() ? 0u : it->second; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 7bc2326f4..9f1d52007 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -24,7 +24,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto userCount = (readRegister(queue.shader, 0x213) >> 1u) & 0x1fu; std::vector userData; for (std::uint32_t i = 0; i < userCount; ++i) { - userData.push_back(readRegister(queue.shader, 0x240 + i)); + userData.push_back(readUserData(queue.shader, 0x240 + i)); } auto compute = Graphics::DecodeComputeStageInfo(queue.shader); std::vector memory{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 42a10f51d..f3d2e0120 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -40,7 +40,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su }; for (std::uint32_t i = 0; i < userCount; ++i) { Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i); - result.userData.push_back(readRegister(queue.shader, userDataBase + i)); + result.userData.push_back(readUserData(queue.shader, userDataBase + i)); } return result; }; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index ebaa9c657..675c0a156 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -867,6 +868,14 @@ void testAsyncMemoryFailure() { } +void testUnwrittenUserData() { + AgcDriver::Registers shader{{0x8c, 0x100}, {0x8d, 0}, {0x240, 0x200}}; + check(AgcDriver::DriverDetail::readUserData(shader, 0x8c) == 0x100 && AgcDriver::DriverDetail::readUserData(shader, 0x240) == 0x200, "a written user data register was not read"); + check(AgcDriver::DriverDetail::readUserData(shader, 0x8d) == 0, "a user data register written as zero was not read"); + check(AgcDriver::DriverDetail::readUserData(shader, 0x95) == 0 && AgcDriver::DriverDetail::readUserData(shader, 0x241) == 0, "an unwritten user data register does not read zero"); + expectFailure([&] { static_cast(AgcDriver::DriverDetail::readRegister(shader, 0x95)); }, "required shader register"); +} + int main(int argc, char** argv) { try { if (argc == 2 && std::string(argv[1]) == "failure") { @@ -893,6 +902,7 @@ int main(int argc, char** argv) { testConditionalWrite(); testConditionalWriteSubmission(); testPredication(); + testUnwrittenUserData(); testDriverSubmission(); testPredicatedSubmission(); testConditionalSubmission(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b9f7399c8..57efbb25a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -63,6 +63,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` - [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted +- [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. From bdfd0287e1d5b57f600d0e3a9815093b0710af89 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 6 Oct 2026 23:43:01 -0300 Subject: [PATCH 158/203] feat(libSceAgc): implement sceAgcGetRegisterDefaultsInternal Return the version 0 internal register defaults, the internal counterpart of sceAgcGetRegisterDefaults. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp | 4 ++++ core/libs/prx/libSceAgc/tests/Commands.cpp | 6 ++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp index 820fb9a77..d5ff3372f 100644 --- a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp @@ -14,6 +14,10 @@ void* APS5_VABI sceAgcGetRegisterDefaults() { return Agc::Command::GetRegisterDefaults(0, false, __func__); } +void* APS5_VABI sceAgcGetRegisterDefaultsInternal() { + return Agc::Command::GetRegisterDefaults(0, true, __func__); +} + void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t version) { return Agc::Command::GetRegisterDefaults(version, false, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 592be7022..7969fdf27 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -14,6 +14,9 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std: extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" int APS5_VABI sceAgcSuspendPoint(); extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); +extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); +extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal(); +extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version); extern "C" int APS5_VABI sceAgcUnknownInitState(std::uint32_t* state, std::uint32_t version); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier); @@ -351,6 +354,9 @@ void testDefaults() { check(first != nullptr && first == Agc::Command::GetRegisterDefaults(version, internal, __func__), "unstable register defaults pointer"); } } + auto* internalDefaults = sceAgcGetRegisterDefaultsInternal(); + check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table"); + check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table"); expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); }); expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b9f7399c8..d36ca93bb 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -37,6 +37,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted +- [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - output mode encoding unknown: every mode except the default reports not supported (0), and `sceVideoOutConfigureOutput` returns `VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE` for it, instead of returning `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` for values the console does not define. PPSA12544 probes `0xd000000a` and selects 7680 pixels when it is supported, 3840 otherwise - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown From 2b83a533b879cdfdaf4392badf8998a9e6c47eb4 Mon Sep 17 00:00:00 2001 From: Nahuel Genchi <65604505+NahuelGenchi@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:00:12 -0300 Subject: [PATCH 159/203] test(libc): match the immortal classic locale and run guest_locale in ctest --- core/libs/prx/libc/CMakeLists.txt | 7 +++++++ core/libs/tests/GuestLocale.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index f40f189ab..c59665ab8 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -286,6 +286,13 @@ target_include_directories(guest_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(guest_locale_tests PROPERTIES CXX_EXTENSIONS OFF) target_link_libraries(guest_locale_tests PRIVATE libc) configure_windows_unwind(guest_locale_tests) +if(BUILD_TESTING) + set_target_properties(guest_locale_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_locale COMMAND guest_locale_tests) + if(WIN32) + set_tests_properties(guest_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index dc1635dd9..632670181 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -57,7 +57,7 @@ static void CheckGuestCalls() { Require(locale->base.references == initial + 1); Require(release(&locale->base) == nullptr); Require(locale->base.references == initial); - RequireException([&] { release(&locale->base); }); + Require(release(&locale->base) == nullptr && locale->base.references == initial); RequireException([&] { retain(nullptr); }); RequireException([&] { locale->base.vtable->deleteObject(&locale->base); }); Require(locale->base.references == initial); From b8bdd24a052e4f947f5a17396eb0714491cc8456 Mon Sep 17 00:00:00 2001 From: Nahuel Genchi <65604505+NahuelGenchi@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:07:00 -0300 Subject: [PATCH 160/203] test(agc): drop the recompiler stub test --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 16 --- .../prx/libSceAgcDriver/tests/Recompiler.cpp | 100 ------------------ 2 files changed, 116 deletions(-) delete mode 100644 core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6fec2dd1f..004f06c57 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -240,22 +240,6 @@ target_link_libraries(agc_driver_tests PRIVATE set_target_properties(agc_driver_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_tests) -add_test_executable(agc_driver_recompiler_tests EXCLUDE_FROM_ALL - tests/Recompiler.cpp -) - -target_include_directories(agc_driver_recompiler_tests PRIVATE - ${LIBS_INCLUDE_DIR} -) - -target_link_libraries(agc_driver_recompiler_tests PRIVATE - libSceAgcDriver - libc -) - -set_target_properties(agc_driver_recompiler_tests PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(agc_driver_recompiler_tests) - add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL tests/ShaderMemory.cpp Execution/src/ShaderMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp b/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp deleted file mode 100644 index e7860d456..000000000 --- a/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" -#include "prx/libc/include/Shutdown.hpp" -#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" -#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" -#include -#include -#include -#include -#include - -int main(int argc, char** argv) { - try { - alignas(256) const std::array code{0xbf810000}; - Shader shader{}; - shader.file_header = 0x34333231; - shader.version = 0x18; - shader.header_size = sizeof(Shader); - shader.shader_size = sizeof(code); - shader.code = code.data(); - AgcDriverRegisterShader_nid_postfix(&shader); - const auto address = reinterpret_cast(code.data()); - std::vector commands{ - 0xc0027600, 0x20c, static_cast(address >> 8u), static_cast(address >> 40u), - 0xc0017600, 0x213, 0, - 0xc0031500, 1, 1, 1, 0x8041 - }; - Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; - const std::array arguments{1, 1, 1}; - alignas(256) const std::array vertexCode{0xbf810000}; - Shader vertexShader = shader; - const std::array indices{0, 1, 2, 0}; - std::uint32_t destination = 0; - const bool graphics = argc == 2 && (std::string(argv[1]) == "draw" || std::string(argv[1]) == "vertex"); - if (graphics) { - shader.type = 1; - AgcDriverRegisterShader_nid_postfix(&shader); - vertexShader.type = 2; - vertexShader.shader_size = sizeof(vertexCode); - vertexShader.code = vertexCode.data(); - AgcDriverRegisterShader_nid_postfix(&vertexShader); - const auto vertexAddress = reinterpret_cast(vertexCode.data()) + 256; - const auto indexAddress = reinterpret_cast(indices.data()); - const auto pixelAddress = std::string(argv[1]) == "vertex" ? 0 : address; - commands = { - 0xc0027600, 0x0c8, static_cast(vertexAddress >> 8u), static_cast(vertexAddress >> 40u), - 0xc0017600, 0x08b, 0, - 0xc0027600, 0x008, static_cast(pixelAddress >> 8u), static_cast(pixelAddress >> 40u), - 0xc0017600, 0x00b, 0x08000002, - 0xc0217600, 0x00c - }; - for (std::uint32_t i = 0; i < 33; ++i) commands.push_back(100 + i); - const auto destinationAddress = reinterpret_cast(&destination); - const std::array draw{ - 0xc0012600, static_cast(indexAddress), static_cast(indexAddress >> 32u), - 0xc0033500, 3, 1, 3, 0x20, - 0xc0033700, 0x100, static_cast(destinationAddress), static_cast(destinationAddress >> 32u), 7 - }; - commands.insert(commands.end(), draw.begin(), draw.end()); - packet = Packet{commands.data(), static_cast(commands.size()), 0, {}}; - } - if (argc == 2 && std::string(argv[1]) == "indirect") { - const auto argumentAddress = reinterpret_cast(arguments.data()); - commands[7] = 0xc0021600; - commands[8] = static_cast(argumentAddress); - commands[9] = static_cast(argumentAddress >> 32u); - commands[10] = 0x8041; - packet.dw_num = 11; - } - if (graphics) sceAgcDriverSubmitDcb(&packet); - else sceAgcDriverSubmitAcb(0x20, &packet); - try { - AgcDriverWaitIdle_nid_postfix(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) != "ShaderRecompiler::Recompile not implemented") { - throw; - } - if (destination != 0) throw std::runtime_error("failed draw executed a later memory write"); - std::puts("Vulkan device initialized; real recompiler exception propagated"); - try { - AgcDriverWaitIdle_nid_postfix(); - throw std::runtime_error("idle lost recompiler failure"); - } catch (const std::runtime_error& idle) { - if (std::string(idle.what()) != "ShaderRecompiler::Recompile not implemented") throw; - } - try { - LibcRunShutdown_nid_postfix(); - throw std::runtime_error("shutdown lost recompiler failure"); - } catch (const std::runtime_error& shutdown) { - if (std::string(shutdown.what()) != "ShaderRecompiler::Recompile not implemented") throw; - } - return 0; - } - throw std::runtime_error("dispatch unexpectedly completed without a recompiler"); - } catch (const std::exception& error) { - std::fprintf(stderr, "%s\n", error.what()); - try { LibcRunShutdown_nid_postfix(); } - catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } - return 1; - } -} From b567bb8eed070670bc3ce59a71abf0916c7fafb6 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 21:19:39 +0200 Subject: [PATCH 161/203] feat(libkernel): accept the audio coprocessor memory protection bits A title maps direct memory with protection 0x3f2, which adds bits 0x100 and 0x200 to the CPU, GPU and AMPR bits, and the mapping threw. They are the audio coprocessor (ACP) read and write bits, as named in the KytyPS5 and etaHEN headers. The coprocessor work runs on the host CPU, so they map to host read and read/write access like the GPU bits. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/DirectMemory/DirectMemory.cpp | 6 +++--- core/libs/tests/GuestMemory.cpp | 18 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 7c58e56f3..8fed7f059 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -194,13 +194,13 @@ void ValidateRange(const void* addr, size_t len, size_t alignment) { } int LinuxProtFromSce(int prot) { - if ((prot & ~0xF7) != 0) { + if ((prot & ~0x3F7) != 0) { // return SCE_KERNEL_ERROR_EINVAL; throw std::invalid_argument("Unsupported memory protection bits: " + std::to_string(prot)); } int result = PROT_NONE; - if (prot & 0x13) result |= PROT_READ; - if (prot & 0x22) result |= PROT_READ | PROT_WRITE; + if (prot & 0x113) result |= PROT_READ; + if (prot & 0x222) result |= PROT_READ | PROT_WRITE; if (prot & 4) result |= PROT_READ | PROT_EXEC; return result; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 2fe9b8b06..59a078b5f 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -125,6 +125,23 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMunmap(first, length) == 0); } +static void CheckAudioCoprocessorProtection() { + constexpr std::size_t length = 0x4000; + void* writable = nullptr; + Require(sceKernelMapFlexibleMemory(&writable, length, 0x200, 0) == 0); + static_cast(writable)[length - 1] = 7; + Require(static_cast(writable)[length - 1] == 7); + Require(sceKernelMprotect(writable, length, 0x100) == 0); + Require(static_cast(writable)[length - 1] == 7); + Require(sceKernelMprotect(writable, length, 0x3f2) == 0); + static_cast(writable)[0] = 9; + Require(sceKernelMunmap(writable, length) == 0); + bool rejected = false; + void* undefined = nullptr; + try { sceKernelMapFlexibleMemory(&undefined, length, 0x400, 0); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected && undefined == nullptr); +} + static void CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -921,6 +938,7 @@ int main() { CheckInternalNamedFlexibleMapping(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); + CheckAudioCoprocessorProtection(); CheckDirectMemoryFollowsPhysicalPages(); CheckReleaseDirectMemoryClearsMappings(); CheckFixedVirtualReservation(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 07ab6b6ad..32a50b9d6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -88,6 +88,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature - [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. Not implemented on Windows - [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified +- [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written From e91fe0f063e11bc7271f87adbe7143d27a533bad Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 20:22:33 +0200 Subject: [PATCH 162/203] feat(libs): declare the exports PPSA14632 imports that no library provides LEGO Horizon Adventures imports 19 functions that no library exports and no open pull request adds, so it fails eager binding. They are declared as throwing stubs. The two libSceAjmWVorbis imports have no known name and are exported by NID from libSceAjm.native. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAcm/Export.cpp | 10 ++++++++++ core/libs/prx/libSceAjm.native/Export.cpp | 12 +++++++++++ .../prx/libSceGameLiveStreaming/Export.cpp | 10 ++++++++++ core/libs/prx/libSceNet/Export.cpp | 5 +++++ .../prx/libSceNpEntitlementAccess/Export.cpp | 20 +++++++++++++++++++ core/libs/prx/libSceNpManager/Export.cpp | 5 +++++ core/libs/prx/libSceNpWebApi2/Export.cpp | 5 +++++ core/libs/prx/libSceVoiceQoS/Export.cpp | 5 +++++ .../prx/libSceWebBrowserDialog/Export.cpp | 10 ++++++++++ core/libs/prx/libkernel/Aio/src/Aio.cpp | 5 +++++ core/libs/prx/libkernel/File/src/Open.cpp | 5 +++++ .../libs/prx/libkernel/Pthread/Posix/Attr.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 2 ++ 13 files changed, 99 insertions(+) diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index db47c5fea..e62e5934d 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -53,4 +53,14 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { return 0; } +int APS5_VABI sceAcm_FFT() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAcm_Panner() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 3585560a0..fcab9e41a 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -55,4 +55,16 @@ const char* APS5_VABI sceAjmStrError(int error) { return nullptr; } +APS5_EXPORT("Z4HDpe1ZEa4", sceAjmWVorbisUnknown00); +int APS5_VABI sceAjmWVorbisUnknown00(void) { + NotImplemented_nid_no_patch("Z4HDpe1ZEa4"); + return 0; +} + +APS5_EXPORT("oXShHcqMcX0", sceAjmWVorbisUnknown01); +int APS5_VABI sceAjmWVorbisUnknown01(void) { + NotImplemented_nid_no_patch("oXShHcqMcX0"); + return 0; +} + } diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index be247e398..11d59f501 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -14,4 +14,14 @@ int APS5_VABI sceGameLiveStreamingTerminate(void) { return 0; } +int APS5_VABI sceGameLiveStreamingGetCurrentStatus2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingGetProgramInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index c93975c43..0b279067d 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1053,4 +1053,9 @@ int APS5_VABI sceNetResolverAbort(void) { return 0; } +int APS5_VABI sceNetResolverStartNtoaMultipleRecords() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 61f27d29e..ccf92707c 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -149,4 +149,24 @@ int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { return 0; } +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index ca76483a5..f48425a3f 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -191,4 +191,9 @@ int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { return 0; } +int APS5_VABI sceNpGetUserIdByAccountId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 7e9445433..2c3e02d70 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -153,4 +153,9 @@ int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { return 0; } +int APS5_VABI sceNpWebApi2SetRequestTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 9cc97fe93..0aa4ef2cb 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -82,4 +82,9 @@ int APS5_VABI sceVoiceQoSWritePacket() { return 0; } +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 1f281f370..dd35e045a 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -69,4 +69,14 @@ int APS5_VABI sceWebBrowserDialogSetCookie(void) { return 0; } +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogResetCookie() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 4811010a6..0dc7d97d8 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -306,4 +306,9 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec } } +int APS5_VABI sceKernelAioPollRequests() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49d2b44cb..2c93a44c4 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -184,4 +184,9 @@ int APS5_VABI sceKernelUnlink(const char* path) { return 0; } +int APS5_VABI sceKernelFcntl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index d9308e40c..5a027d874 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -110,4 +110,9 @@ int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, size_t st return PosixThread::ToErrno(scePthreadAttrSetstacksize(attr, stack_size)); } +int APS5_VABI pthread_attr_setsolosched_np_nid_postfix() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 07ab6b6ad..8d220f7a3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -125,6 +125,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp), and the Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation, have unknown signatures +- [Z4HDpe1ZEa4, oXShHcqMcX0](../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown name, signature +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [NGS2 mastering voice gain](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws From 928cf9b42df10d98b164d294a995136b97fb4215 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:42:17 +0000 Subject: [PATCH 163/203] fix(savedata): serialize mount slot access in libSceSaveData.native Mount, unmount, param, icon and mount-info calls read and wrote the global mount slot table without a lock, so two threads mounting at once could take the same slot and one would later fail to unmount its own mount point. Guard the table with a mutex and make the initialization count atomic. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MBXJqzgohLeVAeHsn4Jpm7 --- core/libs/prx/libSceSaveData.native/Export.cpp | 10 +++++++++- core/libs/prx/libSceSaveData.native/SaveData.hpp | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index aba825cf2..785ec69ca 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -24,7 +24,7 @@ static constexpr char SAVE_DIR[] = "_sd"; static std::atomic g_transaction_counter{1}; -static int g_initializations = 0; +static std::atomic g_initializations{0}; static std::string save_root() { return std::string(SAVE_DIR); @@ -306,6 +306,7 @@ static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo if (mount_point == nullptr || info == nullptr) { throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); } + std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } @@ -325,6 +326,7 @@ static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, if (mount_point == nullptr || param_buf == nullptr) { throw std::runtime_error("sceSaveDataGetParam: null argument"); } + std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; @@ -402,6 +404,7 @@ int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDat if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataLoadIcon: null mount_point"); } + std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } @@ -433,6 +436,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result throw std::runtime_error("sceSaveDataMount3: invalid directory name"); } const std::string real_path = save_root() + "/" + dirName; + std::lock_guard lock(g_slots_mutex); for (const auto& used : g_slots) { if (used.used && used.real_path == real_path) { return SAVE_DATA_ERROR_BUSY; @@ -482,6 +486,7 @@ int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const S if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataSaveIcon: null mount_point"); } + std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } @@ -498,6 +503,7 @@ static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, if (mount_point == nullptr || param_buf == nullptr) { throw std::runtime_error("sceSaveDataSetParam: null argument"); } + std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; @@ -651,6 +657,7 @@ int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { } int APS5_VABI sceSaveDataTerminate(void) { + std::lock_guard lock(g_slots_mutex); if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } @@ -681,6 +688,7 @@ static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataUmount2: null mount_point"); } + std::lock_guard lock(g_slots_mutex); int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 15ecf354d..20d02ad5a 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -3,6 +3,7 @@ #include #include +#include #include constexpr int SAVE_DATA_OK = 0; @@ -41,6 +42,7 @@ struct MountSlot { }; inline std::array g_slots; +inline std::mutex g_slots_mutex; inline int find_slot_by_mount_point(const char* mount_point) { for (int i = 0; i < static_cast(SAVE_DATA_MOUNT_SLOTS); i++) { From fb1ef4f4042b29b0da2eae2294b13d1b60e7726d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 08:11:13 +0000 Subject: [PATCH 164/203] refactor(recompiler): share the SPIR-V emission failure helper Nine SPIR-V backend sources each defined an identical FailEmit in an anonymous namespace. Declare it once in SpirvEmitterHelpers.hpp and define it in SpirvEmitterHelpers.cpp; the message is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MBXJqzgohLeVAeHsn4Jpm7 --- .../include/SpirvBackend/SpirvEmitterHelpers.hpp | 2 ++ .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 5 +---- .../SpirvBackend/src/SpirvMemory/SpirvConstants.cpp | 9 +-------- .../SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp | 4 ---- .../SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp | 6 +++--- .../SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp | 4 ---- .../SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp | 5 +---- .../SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp | 4 ---- .../SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp | 5 +---- .../SpirvBackend/src/SpirvMemory/SpirvTypes.cpp | 5 +---- 10 files changed, 10 insertions(+), 39 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 201f4f9ae..caec0c7bb 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -3,9 +3,11 @@ #include "SpirvBackend/SpirvEmitterState.hpp" #include "Optimization/BindingAllocator.hpp" +#include namespace ShaderRecompiler { +[[noreturn]] void FailEmit(const std::string& reason); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 9c7ee7228..b63540f83 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include @@ -11,10 +12,6 @@ namespace ShaderRecompiler { namespace { - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { switch (state.program.Resources().stage) { case IrShaderStage::Compute: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp index ab2d368b0..c4a0e2627 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include #include @@ -6,14 +7,6 @@ namespace ShaderRecompiler { -namespace { - -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - -} - std::uint32_t ConstantU32(SpirvEmitterState& state, std::uint32_t value) { return state.module.Constant(spv::OpConstant, TypeU32(state), value); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b0fee41fa..0fe12f8ef 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -12,10 +12,6 @@ namespace ShaderRecompiler { namespace { -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - IrShaderStage StageOf(const SpirvEmitterState& state) { return state.program.Resources().stage; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c7b0c1c01..0edb9069a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -12,12 +12,12 @@ namespace ShaderRecompiler { -namespace { - -[[noreturn]] void FailEmit(const std::string& reason) { +void FailEmit(const std::string& reason) { throw std::runtime_error("SPIR-V module emission failed: " + reason); } +namespace { + const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { if (state.inputInfo.pixel == nullptr) { FailEmit("pixel input info is missing"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp index 0a12e65f2..c2497d5f5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp @@ -15,10 +15,6 @@ namespace ShaderRecompiler { namespace { - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - std::uint32_t UnormToF32Bits(SpirvEmitterState& state, std::uint32_t value, std::uint32_t bits) { const std::uint32_t maximum = (1u << bits) - 1u; const auto zero = EmitCompareU32Constant(state, spv::OpIEqual, value, 0u); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 6ee03acfa..a4e551735 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvInputOutput.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp" @@ -18,10 +19,6 @@ namespace ShaderRecompiler constexpr std::uint32_t PsInputFlatShade = 0x00000400u; constexpr std::uint32_t PixelParameterLimit = 32u; - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - IrShaderStage StageOf(const SpirvEmitterState& state) { return state.program.Resources().stage; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9100ce94f..9b42dde31 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -12,10 +12,6 @@ namespace ShaderRecompiler { namespace { -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - IrShaderStage StageOf(const SpirvEmitterState& state) { return state.program.Resources().stage; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 6532d500d..cde24c0ba 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvEmitter.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include #include #include @@ -13,10 +14,6 @@ namespace ShaderRecompiler namespace { - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - std::uint32_t HostInvocationId(SpirvEmitterState& state) { if (state.subgroupLocalInvocationIdVariable == 0) { FailEmit("SubgroupLocalInvocationId was not declared before function emission"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp index acefd6b60..bdc7ced72 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include #include @@ -8,10 +9,6 @@ namespace ShaderRecompiler { namespace { -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - std::uint32_t StorageBufferType(SpirvEmitterState& state) { const auto array = state.module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, TypeU32(state)); return state.module.DecoratedType(spv::OpTypeStruct, From 5625fe6043ac5c9db2396e0abcc1cf912b314bc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 06:19:46 +0000 Subject: [PATCH 165/203] refactor(libSceAvPlayer): build libSceAvPlayer.native from the shared Export.cpp libSceAvPlayer.native/Export.cpp was a byte-identical copy of libSceAvPlayer/Export.cpp. Point add_sce_avplayer_library at the guest directory's Export.cpp, as the Audiodec and Font library functions do, and remove the copy. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017pTZqAyVSZy2FZmaqakPHZ --- .../libs/prx/libSceAvPlayer.native/Export.cpp | 183 ------------------ .../prx/libSceAvPlayer/AvPlayerLibrary.cmake | 2 +- 2 files changed, 1 insertion(+), 184 deletions(-) delete mode 100644 core/libs/prx/libSceAvPlayer.native/Export.cpp diff --git a/core/libs/prx/libSceAvPlayer.native/Export.cpp b/core/libs/prx/libSceAvPlayer.native/Export.cpp deleted file mode 100644 index 25dfc65c5..000000000 --- a/core/libs/prx/libSceAvPlayer.native/Export.cpp +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libSceAvPlayer/include/AvPlayer.hpp" - -using namespace AvPlayer; - -namespace { - -Player* ToPlayer(AvPlayerInternal* h) { - return static_cast(h); -} - -std::uint32_t VideoBufferCount(std::int32_t requested) { - return static_cast(std::clamp(requested, 2, 16)); -} - -} - -#pragma GCC visibility push(default) -extern "C" { - -int APS5_VABI sceAvPlayerAddSource(AvPlayerInternal* h, const char* filename) { - if (!h || !filename) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->AddSource(filename, SourceTypeUnknown); -} - -int APS5_VABI sceAvPlayerAddSourceEx(AvPlayerInternal* h, uint32_t uri_type, const AvPlayerSourceDetails* source_details) { - if (!h || uri_type != 0 || !source_details || !source_details->uri.name) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->AddSource(std::string_view(source_details->uri.name, source_details->uri.length), source_details->source_type); -} - -int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal* h, uint32_t old_stream_id, uint32_t new_stream_id) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->ChangeStream(old_stream_id, new_stream_id); -} - -int APS5_VABI sceAvPlayerClose(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - delete ToPlayer(h); - return SCE_OK; -} - -uint64_t APS5_VABI sceAvPlayerCurrentTime(AvPlayerInternal* h) { - if (!h) return static_cast(static_cast(SCE_AVPLAYER_ERROR_INVALID_PARAMS)); - return ToPlayer(h)->CurrentTime(); -} - -int APS5_VABI sceAvPlayerDisableStream(AvPlayerInternal* h, uint32_t stream_id) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->DisableStream(stream_id); -} - -int APS5_VABI sceAvPlayerEnableStream(AvPlayerInternal* h, uint32_t stream_id) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->EnableStream(stream_id); -} - -Bool APS5_VABI sceAvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo* audio_info) { - if (!h || !audio_info) return false; - return ToPlayer(h)->GetAudioData(*audio_info); -} - -int APS5_VABI sceAvPlayerGetStreamInfo(AvPlayerInternal* h, uint32_t stream_id, AvPlayerStreamInfo* info) { - if (!h || !info) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->GetStreamInfo(stream_id, *info); -} - -int APS5_VABI sceAvPlayerGetStreamInfoEx(AvPlayerInternal* h, uint32_t stream_id, AvPlayerStreamInfoEx* info) { - if (!h || !info) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->GetStreamInfoEx(stream_id, *info); -} - -Bool APS5_VABI sceAvPlayerGetVideoData(AvPlayerInternal* h, AvPlayerFrameInfo* video_info) { - if (!h || !video_info) return false; - return ToPlayer(h)->GetVideoData(*video_info); -} - -Bool APS5_VABI sceAvPlayerGetVideoDataEx(AvPlayerInternal* h, AvPlayerFrameInfoEx* video_info) { - if (!h || !video_info) return false; - return ToPlayer(h)->GetVideoData(*video_info); -} - -AvPlayerInternal* APS5_VABI sceAvPlayerInit(AvPlayerInitData* init) { - if (!init) return nullptr; - return new Player(*init, VideoBufferCount(init->num_output_video_framebuffers)); -} - -int APS5_VABI sceAvPlayerInitEx(const AvPlayerInitDataEx* init_ex, AvPlayerInternal** handle) { - if (!init_ex || !handle) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - AvPlayerInitData init{}; - init.memory_replacement = init_ex->memory_replacement; - init.file_replacement = init_ex->file_replacement; - init.event_replacement = init_ex->event_replacement; - init.debug_level = init_ex->debug_level; - init.num_output_video_framebuffers = init_ex->num_output_video_framebuffers; - init.auto_start = init_ex->auto_start; - init.default_language = init_ex->default_language; - *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers)); - return SCE_OK; -} - -Bool APS5_VABI sceAvPlayerIsActive(AvPlayerInternal* h) { - if (!h) return false; - return ToPlayer(h)->IsActive(); -} - -int APS5_VABI sceAvPlayerJumpToTime(AvPlayerInternal* h, uint64_t time_ms) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->JumpToTime(time_ms); -} - -int APS5_VABI sceAvPlayerPause(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Pause(); -} - -int APS5_VABI sceAvPlayerPostInit(AvPlayerInternal* h, const AvPlayerPostInitData* post_init) { - if (!h || !post_init) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->PostInit(*post_init); -} - -int APS5_VABI sceAvPlayerResume(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Resume(); -} - -int APS5_VABI sceAvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->SetAvSyncMode(sync_mode); -} - -int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t start_bandwidth, uint32_t minimum_bandwidth, uint32_t maximum_bandwidth) { - (void)start_bandwidth; - (void)minimum_bandwidth; - (void)maximum_bandwidth; - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return SCE_OK; -} - -int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - return SCE_OK; -} - -int APS5_VABI sceAvPlayerSetLooping(AvPlayerInternal* h, Bool loop) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->SetLooping(loop != 0); -} - -int APS5_VABI sceAvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->SetTrickSpeed(trick_speed); -} - -int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Start(); -} - -int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex) { - (void)start_info_ex; - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Start(); -} - -int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Stop(); -} - -int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->StreamCount(); -} - -} -#pragma GCC visibility pop diff --git a/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake b/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake index 8facef190..86a5d13d3 100644 --- a/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake +++ b/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake @@ -1,7 +1,7 @@ function(add_sce_avplayer_library target) set(avPlayerDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) add_library(${target} SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${avPlayerDir}/Export.cpp ${avPlayerDir}/src/Player.cpp ${avPlayerDir}/src/Source.cpp ) From c8e4a17f89c0738c92cb66f1e646415ae494ccf1 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:33:43 +0200 Subject: [PATCH 166/203] feat(shader): decode s_setkill as s_endpgm --- .../tests/execution/ScalarProgramEnd.cpp | 12 +++++++++--- .../RdnaDecoder/src/RdnaScalarOpDecoder.cpp | 1 + docs/dev/TechnicalDebt.md | 1 + tools/progress.py | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp index a4362f4c3..335a99a9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp @@ -30,6 +30,11 @@ alignas(256) constexpr std::array Code{ 0xe0701000, 0x80010603, 0xbf9b0000, 0xe0701004, 0x80010603, 0xbf810000, }; +alignas(256) constexpr std::array SetkillCode{ + 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x4a0a0881, 0x7e0c02ff, 0x00000bad, 0xbf060000, 0xbf840007, + 0xbf840003, 0xe0701000, 0x80010503, 0xbf8b0000, 0xe0701000, 0x80010603, 0xbf8b0001, 0xe0701004, 0x80010603, 0xbf810000, +}; + alignas(256) constexpr std::array RoundTowardZeroCode{0xbfa40003, 0xbf810000}; void Fill(std::uint32_t tid, std::uint32_t* words) { words[0] = tid * 0x01010101u + 7u; } @@ -74,7 +79,7 @@ void ExpectRefused(AgcDriver::VulkanDevice& device) { Require(refused, "scalar program end: s_round_mode 3 was translated"); } -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::span code) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); @@ -82,7 +87,6 @@ void Run(AgcDriver::VulkanDevice& device) { const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const auto result = ShaderRecompiler::Recompile(Request(device, code, userData, memory)); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); @@ -104,7 +108,9 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); + Run(*device, Code); + Check(); + Run(*device, SetkillCode); Check(); ExpectRefused(*device); std::puts("scalar program end tests passed"); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index de3c85f62..d774eba52 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -201,6 +201,7 @@ RdnaOpcode decodeSoppOpcode(std::uint32_t opcode) { case 0x08u: return RdnaOpcode::SCbranchExecz; case 0x09u: return RdnaOpcode::SCbranchExecnz; case 0x0au: return RdnaOpcode::SBarrier; + case 0x0bu: return RdnaOpcode::SEndpgm; case 0x0cu: return RdnaOpcode::SWaitcnt; case 0x0eu: return RdnaOpcode::SSleep; case 0x0fu: return RdnaOpcode::SSetprio; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 07ab6b6ad..0075459c5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -44,6 +44,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - output mode encoding unknown: every mode except the default reports not supported (0), and `sceVideoOutConfigureOutput` returns `VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE` for it, instead of returning `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` for values the console does not define. PPSA12544 probes `0xd000000a` and selects 7680 pixels when it is supported, 3840 otherwise - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown +- S_SETKILL translation (core/shader/recompiler) - decoded as `s_endpgm`: measured on RDNA2 hardware (gfx1035) that `s_setkill` with any immediate ends the wave exactly like `s_endpgm`, with a store before it visible and nothing after it executed, matching the `s_endpgm_saved` treatment from [#436](https://github.com/boykopovar/AnyPS5/pull/436); whether the KILL status bit has an observable difference from END_PG status is unverified. S_SETHALT, S_SENDMSGHALT and S_CODE_END stay unimplemented and throw: halt freezes the wave until a debugger or the trap handler resumes it (the dispatch never completes; `s_sendmsghalt` sends its message and then halts the same way), which SPIR-V cannot represent, and `s_code_end` padding is excluded from translation by program decoding while executing it hangs the wave (AMD ISA GCN3 §12.4/§13.1 and the same measurement) - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`), plus the RFC 2822 shape `Ddd, DD Mon YYYY HH:MM:SS[ GMT| ±HHMM]` and the asctime shape `Ddd Mon DD HH:MM:SS YYYY` (day zero- or space-padded) that shadPS4 parses at fixed positions; the weekday name is not checked against the date, a `±HHMM` offset is subtracted to get UTC like the RFC 3339 path (shadPS4 adds it), a string without a zone is taken as UTC, and anything outside these shapes throws instead of returning a parse error, unverified against hardware - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order diff --git a/tools/progress.py b/tools/progress.py index a0b5ae4e4..fcb383408 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -26,7 +26,7 @@ OPCODE_ALIASES = { "VMadMixhiF16": "V_FMA_MIXHI_F16", } OPCODE_VARIANTS = { - "SEndpgm": ("S_ENDPGM_SAVED", "S_ENDPGM_ORDERED_PS_DONE"), + "SEndpgm": ("S_ENDPGM_SAVED", "S_ENDPGM_ORDERED_PS_DONE", "S_SETKILL"), "VFmaF32": ("V_FMA_MIX_F32",), "SAddI32": ("S_ADDK_I32",), "SCmpEqI32": ("S_CMPK_EQ_I32",), From c50181f6fa5d4a47d4a35a37327ffdd56a260fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 08:39:47 +0200 Subject: [PATCH 167/203] docs(TechnicalDebt): trim s_setkill entry to the measured behaviour Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0075459c5..c8561af00 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -44,7 +44,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - output mode encoding unknown: every mode except the default reports not supported (0), and `sceVideoOutConfigureOutput` returns `VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE` for it, instead of returning `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` for values the console does not define. PPSA12544 probes `0xd000000a` and selects 7680 pixels when it is supported, 3840 otherwise - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown -- S_SETKILL translation (core/shader/recompiler) - decoded as `s_endpgm`: measured on RDNA2 hardware (gfx1035) that `s_setkill` with any immediate ends the wave exactly like `s_endpgm`, with a store before it visible and nothing after it executed, matching the `s_endpgm_saved` treatment from [#436](https://github.com/boykopovar/AnyPS5/pull/436); whether the KILL status bit has an observable difference from END_PG status is unverified. S_SETHALT, S_SENDMSGHALT and S_CODE_END stay unimplemented and throw: halt freezes the wave until a debugger or the trap handler resumes it (the dispatch never completes; `s_sendmsghalt` sends its message and then halts the same way), which SPIR-V cannot represent, and `s_code_end` padding is excluded from translation by program decoding while executing it hangs the wave (AMD ISA GCN3 §12.4/§13.1 and the same measurement) +- [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`), plus the RFC 2822 shape `Ddd, DD Mon YYYY HH:MM:SS[ GMT| ±HHMM]` and the asctime shape `Ddd Mon DD HH:MM:SS YYYY` (day zero- or space-padded) that shadPS4 parses at fixed positions; the weekday name is not checked against the date, a `±HHMM` offset is subtracted to get UTC like the RFC 3339 path (shadPS4 adds it), a string without a zone is taken as UTC, and anything outside these shapes throws instead of returning a parse error, unverified against hardware - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order From 26e153dfa208b8f6e3b534b7644ec19bdee67e9e Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:22:23 +0200 Subject: [PATCH 168/203] feat(agc): implement 8_sint sampled images GFX10 format 6 (8_SINT) T#s threw "sampled image descriptor uses an unsupported format": neither the recompiler's format table nor the driver's texture format table had a row for it. Format 6 now reads as a SINT image through an R8_SINT view, so image_load sign-extends each texel (0x80 reads -128) and image-table entries of format 6 decode. 8_SINT storage still throws. --- .../Graphics/src/TextureFormat.cpp | 1 + .../libSceAgcDriver/tests/TextureFormat.cpp | 3 + .../tests/execution/ImageSint8.cpp | 241 ++++++++++++++++++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 3 +- 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 108bdbe5d..2fa46b416 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -18,6 +18,7 @@ struct FormatEntry { constexpr FormatEntry kFormatLookup[] = { {1, VK_FORMAT_R8_UNORM, 1, false}, {5, VK_FORMAT_R8_UINT, 1, false}, + {6, VK_FORMAT_R8_SINT, 1, false}, {7, VK_FORMAT_R16_UNORM, 2, false}, {8, VK_FORMAT_R16_SNORM, 2, false}, {11, VK_FORMAT_R16_UINT, 2, false}, diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 3973bfa18..7a273136d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -51,6 +51,9 @@ void RunTextureFormatTests() { Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM"); Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide"); + Require(ResolveTextureFormat(6) == VK_FORMAT_R8_SINT, "format 6 must resolve to R8_SINT"); + Require(BytesPerElement(6) == 1u, "format 6 must be one byte wide"); + Require(ResolveTextureFormat(22) == VK_FORMAT_R32_SFLOAT, "format 22 must resolve to R32_SFLOAT"); Require(BytesPerElement(22) == 4u, "format 22 must be four bytes wide"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp new file mode 100644 index 000000000..87dd6105b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp @@ -0,0 +1,241 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t Texels = Width * Height; +constexpr std::uint32_t Sint8 = 6; +constexpr std::uint32_t Unorm8 = 1; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t SwizzleX111 = 0x24cu; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; +constexpr std::array LinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u}; + +alignas(256) std::array Output{}; +alignas(256) std::array Texture{}; + +constexpr std::array TexelCode(std::uint32_t word0) { + return { + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x2c3e3c87u, 0x363c3cffu, 0x0000007fu, 0x7e140280u, + 0x7e160280u, 0x7e180280u, 0x7e1a0280u, word0, 0x00010a1eu, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, + 0xbf810000u, + }; +} + +constexpr std::array SamplerCode(std::uint32_t word0, std::uint32_t u, std::uint32_t v) { + return { + 0x34060084u, 0x7e2802ffu, u, 0x7e2a02ffu, v, word0, 0x00610a14u, 0xbf8c3f70u, + 0xe0781000u, 0x80000a03u, 0xbf810000u, + }; +} + +alignas(256) constexpr auto LoadX = TexelCode(0xf0001108u); +alignas(256) constexpr auto LoadXyzw = TexelCode(0xf0001f08u); +alignas(256) constexpr auto StoreX = TexelCode(0xf0201108u); +alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u, 0x3ce00000u, 0x3d300000u); +alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u, 0x3c000000u, 0x3c000000u); + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t PitchBytes() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Unorm8, Width, Height, 1u); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texture.size(), "image 8_SINT: the texture does not fit its storage"); + return mips[0].pitchBytes; +} + +std::uint8_t& Texel(std::uint32_t x, std::uint32_t y) { + static const auto pitch = PitchBytes(); + return Texture[static_cast(y) * pitch + x]; +} + +std::uint32_t Signed(std::uint32_t x, std::uint32_t y) { + return static_cast(static_cast(static_cast(Texel(x, y)))); +} + +void Fill() { + Texture.fill(0x5au); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + Texel(x, y) = static_cast(0x80u + 37u * x + 11u * y); + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t records, std::uint32_t stride = 0u) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, 0x01016facu}; +} + +std::array TextureDescriptor(const void* texels, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((height - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t format, const std::array& sampler, std::uint32_t groups) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto texture = TextureDescriptor(Texture.data(), format, Width, Height, SwizzleX111); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Expect(std::uint32_t slot, const std::array& expected, const std::string& what) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto actual = Output[slot * 4u + component]; + Require(actual == expected[component], what + ": result " + std::to_string(slot) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component])); + } +} + +void CheckLoads(AgcDriver::VulkanDevice& device) { + Fill(); + Require(Texel(0, 0) == 0x80u && Signed(0, 0) == 0xffffff80u, "image 8_SINT: texel (0, 0) must hold 0x80"); + Run(device, LoadX, Sint8, PointSampler, Texels / Threads); + for (std::uint32_t texel = 0; texel < Texels; ++texel) { + Expect(texel, {Signed(texel % Width, texel / Width), 0u, 0u, 0u}, "8_SINT image_load dmask:0x1"); + } + Run(device, LoadXyzw, Sint8, PointSampler, Texels / Threads); + for (std::uint32_t texel = 0; texel < Texels; ++texel) { + Expect(texel, {Signed(texel % Width, texel / Width), 1u, 1u, 1u}, "8_SINT image_load dmask:0xf X 1 1 1"); + } +} + +void CheckSamples(AgcDriver::VulkanDevice& device) { + Fill(); + Run(device, SampleLz, Sint8, PointSampler, 1); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Expect(lane, {Signed(3, 5), 1u, 1u, 1u}, "8_SINT image_sample_lz with point filtering"); + } + Run(device, Gather4Lz, Sint8, LinearSampler, 1); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Expect(lane, {Signed(0, 1), Signed(1, 1), Signed(1, 0), Signed(0, 0)}, "8_SINT image_gather4_lz through a bilinear sampler"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t format, const std::array& sampler, const std::string& reason, const std::string& what) { + std::string refusal; + try { + Run(device, code, format, sampler, 1); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(reason) != std::string::npos, what + " was not refused: " + refusal); +} + +struct alignas(4096) GuestTables { + std::array, 2> heap{}; + std::array, 3> materials{}; + std::array output{}; + std::array srt{}; +}; + +struct alignas(256) TableTexture { + std::array bytes{}; +}; + +GuestTables Tables; +TableTexture TableTextures[2]; + +alignas(256) const std::array TableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, + 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, + 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u, +}; + +void CheckTable(AgcDriver::VulkanDevice& device) { + TableTextures[0].bytes.fill(0x80u); + TableTextures[1].bytes.fill(0xffu); + for (std::uint32_t entry = 0; entry < 2u; ++entry) Tables.heap[entry] = TextureDescriptor(TableTextures[entry].bytes.data(), Sint8, 4u, 4u, IdentitySwizzle); + const auto heap = BufferDescriptor(Tables.heap.data(), 2u, 32u); + const auto materials = BufferDescriptor(Tables.materials.data(), 3u, 16u); + const auto output = BufferDescriptor(Tables.output.data(), 16u); + std::copy(heap.begin(), heap.end(), Tables.srt.begin()); + std::copy(materials.begin(), materials.end(), Tables.srt.begin() + 8); + std::copy(output.begin(), output.end(), Tables.srt.begin() + 12); + const auto srtAddress = static_cast(reinterpret_cast(Tables.srt.data())); + const std::vector userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::span code(TableCode); + const std::array memory{{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(&Tables), std::as_bytes(std::span(&Tables, 1))}, + }}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{8, 1, 1}, 0u, {false, false, false}, false, 1}; + for (const std::uint32_t key : {0u, 1u}) { + Tables.materials = {{{0u, key, 0u, 0u}, {0u, 0u, 0u, 0u}, {0u, 1u, 0u, 0u}}}; + Tables.output.fill(0xdeadbeefu); + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + const auto expected = key == 0u ? 0xffffff80u : 0xffffffffu; + Require(Tables.output[0] == expected, "8_SINT image table entry " + std::to_string(key) + ": image_sample_lz read " + Hex(Tables.output[0]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckLoads(*device); + CheckSamples(*device); + CheckTable(*device); + RequireRefused(*device, StoreX, Sint8, PointSampler, "storage image descriptor uses an unsupported format", "8_SINT image_store"); + std::puts("image 8_SINT tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 2d68f1a1a..21faa359a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,10 +13,11 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, {IrBufferFormat::Format8SNorm, false, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, + {IrBufferFormat::Format8SInt, true, false, true}, {IrBufferFormat::Format16UNorm, true, false, false}, {IrBufferFormat::Format16SNorm, true, false, false}, {IrBufferFormat::Format16UInt, true, true, false}, From 1afcd202810f0e1f547bd9fcc1a7b8bbca0a34f2 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:17:22 +0200 Subject: [PATCH 169/203] feat(agc): implement min and max sampler reduction modes An S# with FILTER_MODE (word0 bits 29-30) set threw in DecodeSamplerResource. Modes 1 and 2 now map to Vulkan's MIN and MAX sampler reduction through VK_EXT_sampler_filter_minmax, which the device enables when it reports both filterMinmax properties; mode 3 throws. Min/max still throws with anisotropy, a linear mip filter (lavapipe and NVIDIA disagree there), depth comparison, color-texture compare emulation, and, in PointFilteredSamplerWord (S# words 0 and 2), when it filters between texels or mips of an image the recompiler point-samples. A texture sampled through a linear min/max sampler needs SAMPLED_IMAGE_FILTER_MINMAX on its view format; the image-sampler pairs travel in DescriptorBinding::imageSamplers (shader disk cache format 5). Adds the agc_driver_image_sample_reduction GPU test and extends the sampler, emulated color compare and disk cache tests. --- .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/VulkanDevice.cpp | 13 + .../Graphics/include/Context.hpp | 1 + .../Graphics/include/GuestSamplerResource.hpp | 1 + .../Graphics/include/Sampler.hpp | 4 + .../Graphics/include/ShaderResources.hpp | 4 +- .../Graphics/include/Texture.hpp | 2 + .../Graphics/src/GuestSamplerResource.cpp | 14 +- .../libSceAgcDriver/Graphics/src/Sampler.cpp | 25 ++ .../Graphics/src/ShaderResources.cpp | 14 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 3 + .../tests/GuestSamplerResource.cpp | 141 +++++++- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 3 +- .../tests/execution/EmulatedColorCompare.cpp | 5 +- .../tests/execution/ImageSampleReduction.cpp | 319 ++++++++++++++++++ .../Optimization/DescriptorBindingBuilder.hpp | 3 + .../src/DescriptorBindingBuilder.cpp | 42 ++- .../Optimization/src/ResourceMaterializer.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 4 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- docs/dev/TechnicalDebt.md | 2 + 22 files changed, 592 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 0064178b9..9f12df608 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -158,6 +158,7 @@ public: void Resize(std::uint32_t width, std::uint32_t height); bool Presentable() const; bool PrimitiveListRestart() const; + bool SamplerFilterMinmax() const; // A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it // touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command // buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 6b50c2acd..3d848fde1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -204,6 +204,7 @@ struct VulkanDevice::State { bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; bool textureCompressionBC = false; + bool samplerFilterMinmax = false; // VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available. bool timelineSemaphores = false; bool computeWave32 = false; @@ -763,6 +764,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); if (imageRobustness) deviceExtensions.push_back(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME)) { + VkPhysicalDeviceSamplerFilterMinmaxPropertiesEXT minmaxProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_FILTER_MINMAX_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &minmaxProperties}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->samplerFilterMinmax = minmaxProperties.filterMinmaxSingleComponentFormats == VK_TRUE && minmaxProperties.filterMinmaxImageComponentMapping == VK_TRUE; + if (state->samplerFilterMinmax) deviceExtensions.push_back(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME); + } // Indirect draws with a GPU-side count (DRAW_INDIRECT_MULTI with count_indirect); a device // without it resolves such draws on the CPU. state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME); @@ -2341,6 +2349,10 @@ bool VulkanDevice::PrimitiveListRestart() const { return state->primitiveListRestart; } +bool VulkanDevice::SamplerFilterMinmax() const { + return state->samplerFilterMinmax; +} + Graphics::Context VulkanDevice::graphicsContext() const { static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr; if (state->contextReady && !noCache) return state->context; @@ -2383,6 +2395,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.multiDrawIndirect = state->multiDrawIndirect; context.depthBounds = state->depthBounds; context.depthBiasClamp = state->depthBiasClamp; + context.samplerFilterMinmax = state->samplerFilterMinmax; context.drawIndirectCount = state->drawIndirectCount; context.occlusionQueryPrecise = state->occlusionQueryPrecise; context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 238c05424..befeff74c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -123,6 +123,7 @@ struct Context { bool occlusionQueryPrecise = false; bool depthBounds = false; bool depthBiasClamp = false; + bool samplerFilterMinmax = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index f8444bfdb..5312d9b5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -23,6 +23,7 @@ struct GuestSamplerResource { float maxLod; float lodBias; VkBorderColor borderColor; + VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT; bool compareEnable = false; VkCompareOp compareOp = VK_COMPARE_OP_NEVER; bool unnormalizedCoordinates = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 67450c4ac..564679119 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -20,12 +20,14 @@ public: Sampler& operator=(const Sampler&) = delete; VkSampler Handle() const; + bool RequiresFilterMinmax() const; private: void release() noexcept; Context context; VkSampler sampler = VK_NULL_HANDLE; + bool requiresFilterMinmax = false; }; // One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the @@ -57,6 +59,8 @@ private: std::uint64_t misses = 0; }; +void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 69d086ca4..9df5a3c1b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -326,7 +326,7 @@ private: // Stage B: the record's texture when the fastRevalidate predicate proves it current under the // lock and the cache still holds it; null sends the element to cachedTexture. std::shared_ptr fastTexture(const ImageRecord& record); - void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item); + void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers); void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); @@ -436,6 +436,8 @@ private: struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; std::size_t index; + std::size_t firstSampler = 0; + std::size_t samplerCount = 0; }; std::vector deferredImages; std::uint64_t storageBuffers = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 08009e94f..10fb690a3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -75,6 +75,7 @@ public: // The layout the image is kept in while sampled. VkImageLayout Layout() const { return layout; } VkDeviceSize AllocationBytes() const { return allocationBytes; } + VkFormat ViewFormat() const { return viewFormat; } // Whether this texture is a view of a storage image (no snapshot of its own). bool ViewsStorageImage() const { return storageSource != nullptr; } const StorageTexture* StorageSource() const { return storageSource.get(); } @@ -100,6 +101,7 @@ private: ViewRange firstLayerRange{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; + VkFormat viewFormat = VK_FORMAT_UNDEFINED; std::shared_ptr source; std::shared_ptr storageSource; std::unique_ptr upload; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index 5845b80fb..e70e48018 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -20,6 +20,15 @@ bool isAnisoFilter(std::uint32_t raw) { return raw == 2 || raw == 3; } +VkSamplerReductionMode toVkReductionMode(std::uint32_t raw) { + switch (raw) { + case 0: return VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT; + case 1: return VK_SAMPLER_REDUCTION_MODE_MIN_EXT; + case 2: return VK_SAMPLER_REDUCTION_MODE_MAX_EXT; + default: throw std::runtime_error("AGC graphics: guest sampler descriptor uses an unknown reduction filter mode " + std::to_string(raw)); + } +} + VkSamplerAddressMode toVkAddressMode(std::uint32_t raw) { switch (raw) { case 0: return VK_SAMPLER_ADDRESS_MODE_REPEAT; @@ -99,15 +108,17 @@ GuestSamplerResource DecodeSamplerResource(std::span words, static_cast(anisoThreshold); static_cast(anisoBias); Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented"); - Require(filterMode == 0, "guest sampler descriptor uses a reduction filter mode which is not implemented"); + const auto reductionMode = toVkReductionMode(filterMode); Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented"); Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented"); Require(!pointPreclamp, "guest sampler descriptor uses point preclamping which is not implemented"); Require(!anisoOverride, "guest sampler descriptor uses an anisotropy override which is not implemented"); Require(!blendZeroPrt, "guest sampler descriptor uses PRT blend-zero which is not implemented"); if (mipFilter > 2u) Require(false, "guest sampler descriptor uses an unknown mip filter " + std::to_string(mipFilter)); + Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented"); const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter); + Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented"); auto anisoRatio = 1.0f; if (aniso) { if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio)); @@ -147,6 +158,7 @@ GuestSamplerResource DecodeSamplerResource(std::span words, result.maxLod = maxLod; result.lodBias = toSignedLodBias(lodBiasRaw); result.borderColor = border; + result.reductionMode = reductionMode; const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS}; result.compareOp = compareOps.at(depthCompareFunc); if (forceUnormCoords) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index b0bed507f..851ae3bf7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include #include +#include namespace AgcDriver::Graphics { @@ -11,6 +12,13 @@ namespace AgcDriver::Graphics { Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented"); VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + VkSamplerReductionModeCreateInfoEXT reduction{VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT, nullptr, descriptor.reductionMode}; + if (descriptor.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT) { + Require(context.samplerFilterMinmax, "guest sampler descriptor uses a min or max reduction which the device does not support"); + Require(!descriptor.compareEnable, "guest sampler descriptor combines a min or max reduction with depth comparison, which is not implemented"); + info.pNext = &reduction; + requiresFilterMinmax = descriptor.magFilter == VK_FILTER_LINEAR || descriptor.minFilter == VK_FILTER_LINEAR; + } info.magFilter = descriptor.magFilter; info.minFilter = descriptor.minFilter; info.mipmapMode = descriptor.mipmapMode; @@ -41,6 +49,10 @@ namespace AgcDriver::Graphics { return sampler; } + bool Sampler::RequiresFilterMinmax() const { + return requiresFilterMinmax; + } + SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max(capacity, 1)) {} std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven) { @@ -67,4 +79,17 @@ namespace AgcDriver::Graphics { return sampler; } + void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers) { + bool filtered = false; + for (std::uint32_t element = 0; element < 32u; ++element) { + if (((samplerMask >> element) & 1u) == 0u) continue; + if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind"); + filtered = filtered || samplers[element]->RequiresFilterMinmax(); + } + if (!filtered) return; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT) == 0) Require(false, "a sampled texture whose format " + std::to_string(format) + " does not support min/max filtering is sampled through a min or max reduction sampler with linear filtering"); + } + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 759380e79..5f4c021ab 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -899,6 +899,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons std::uint64_t stageDescriptors = 0; std::vector offsetsInData; std::int64_t shaderData = -1; + const auto firstSampler = samplers.size(); + const auto firstDeferred = deferredImages.size(); for (const auto& binding : shader.program->bindings) { Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); Require(occupied.insert(binding.binding).second, "duplicate shader binding"); @@ -948,6 +950,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons bindings.push_back(std::move(item)); } for (const auto index : offsetsInData) allocations[index].dataAllocation = shaderData; + for (auto index = firstDeferred; index < deferredImages.size(); ++index) { + deferredImages[index].firstSampler = firstSampler; + deferredImages[index].samplerCount = samplers.size() - firstSampler; + } } Require(storageBuffers <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits"); timing.bindingsMs = phase(BuildPhase::Bindings); @@ -1015,7 +1021,7 @@ void ShaderResources::buildComplete() { try { // The lookups run in plan order: Revalidate walks the bindings the same way, and consecutive // storage elements of one mip chain share the previous element's image. - for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index]); + for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span>(samplers).subspan(deferred.firstSampler, deferred.samplerCount)); deferredImages.clear(); // The records served their purpose: each holds the cache entry's objects as of stage A, // which would otherwise keep a replaced texture or an evicted storage image (and its device @@ -1213,6 +1219,8 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha // Read-only elements are bound without a write set: an object built for one written set // must not serve a build with another (the variant implies it, this makes it explicit). packBits(binding.bufferWritten); + key.push_back(static_cast(binding.imageSamplers.size())); + key.insert(key.end(), binding.imageSamplers.begin(), binding.imageSamplers.end()); } return key; } @@ -2523,11 +2531,12 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) return record.texture; } -void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item) { +void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers) { auto& counters = TextureCounts(); // The element's stage-A record, when the pass ran (records follow the plan order exactly). const auto nextRecord = [&]() -> const ImageRecord* { return nextImageRecord < imageRecords.size() ? &imageRecords[nextImageRecord++] : nullptr; }; if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { + Require(binding.imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs"); const auto elementWords = binding.guestDescriptor.size() / binding.count; for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); @@ -2548,6 +2557,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f; if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast(range.type)) + ")"); } + RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 513412318..695f83934 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -423,6 +423,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex viewInfo.image = image; viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = vkFormat; + viewFormat = vkFormat; viewInfo.components = depthCompare ? VkComponentMapping{} : components; viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -469,6 +470,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewInfo.image = source->Image(); viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = vkFormat; + viewFormat = vkFormat; viewInfo.components = components; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -494,6 +496,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.image = depthImage; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = depthFormat; + viewFormat = depthFormat; viewInfo.components = components; viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 43e54c2f0..4a423ed0a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -1,9 +1,14 @@ #include "GraphicsTests.hpp" +#include "Optimization/DescriptorBindingBuilder.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include #include +#include +#include #include #include +#include namespace { @@ -84,6 +89,138 @@ bool nearlyEqual(float a, float b) { return std::fabs(a - b) < 0.001f; } +struct SamplerCapture { + bool created = false; + const void* next = nullptr; + VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM; +}; +SamplerCapture samplerCapture; + +template +THandle fakeHandle(std::uint64_t value) { + if constexpr (std::is_pointer_v) return reinterpret_cast(static_cast(value)); + else return static_cast(value); +} + +VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) { + samplerCapture.created = true; + samplerCapture.next = info->pNext; + const auto* reduction = static_cast(info->pNext); + if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode; + *sampler = fakeHandle(0x5a); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL captureDestroySampler(VkDevice, VkSampler, const VkAllocationCallbacks*) {} + +PFN_vkVoidFunction VKAPI_CALL captureProc(VkDevice, const char* name) { + if (std::strcmp(name, "vkCreateSampler") == 0) return reinterpret_cast(captureCreateSampler); + if (std::strcmp(name, "vkDestroySampler") == 0) return reinterpret_cast(captureDestroySampler); + return nullptr; +} + +SamplerCapture createSampler(const Context& context, const GuestSamplerResource& resource) { + samplerCapture = {}; + const Sampler sampler(context, resource); + Require(sampler.Handle() != VK_NULL_HANDLE, "sampler test did not create a sampler"); + return samplerCapture; +} + +void RunSamplerReductionTests(const Fields& base) { + Context context{}; + context.limits.maxSamplerAnisotropy = 1.0f; + context.deviceProc = captureProc; + context.samplerFilterMinmax = true; + + const std::array capturedSampler{0x20000092u, 0x00fff000u, 0x05500000u, 0u}; + const auto captured = DecodeSamplerResource(capturedSampler); + Require(captured.reductionMode == VK_SAMPLER_REDUCTION_MODE_MIN_EXT, "captured min-reduction sampler decoded to another reduction mode"); + Require(captured.magFilter == VK_FILTER_LINEAR && captured.minFilter == VK_FILTER_LINEAR && captured.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST, "captured min-reduction sampler filters decoded incorrectly"); + Require(captured.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured min-reduction sampler address modes decoded incorrectly"); + Require(!captured.anisotropyEnable && captured.compareOp == VK_COMPARE_OP_NEVER && nearlyEqual(captured.maxLod, 4095.0f / 256.0f), "captured min-reduction sampler fields decoded incorrectly"); + + const std::array modes{VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, VK_SAMPLER_REDUCTION_MODE_MIN_EXT, VK_SAMPLER_REDUCTION_MODE_MAX_EXT}; + for (std::uint32_t mode = 0; mode < modes.size(); ++mode) { + for (const std::uint32_t filter : {0u, 1u}) { + Fields reduced = base; + reduced.filterMode = mode; + reduced.xyMagFilter = filter; + reduced.xyMinFilter = filter; + reduced.mipFilter = 1; + const auto decoded = DecodeSamplerResource(pack(reduced)); + Require(decoded.reductionMode == modes.at(mode), "sampler reduction mode decoded incorrectly"); + const auto created = createSampler(context, decoded); + Require(created.created, "sampler test did not reach vkCreateSampler"); + if (mode == 0u) Require(created.next == nullptr, "a weighted-average sampler chained a reduction mode"); + else Require(created.reductionMode == modes.at(mode), "a min or max sampler did not chain its reduction mode"); + const Sampler sampler(context, decoded); + Require(sampler.RequiresFilterMinmax() == (mode != 0u && filter == 1u), "sampler min/max format requirement is wrong"); + } + } + Fields noMip = base; + noMip.filterMode = 2; + noMip.mipFilter = 0; + Require(DecodeSamplerResource(pack(noMip)).reductionMode == VK_SAMPLER_REDUCTION_MODE_MAX_EXT, "a max sampler without mip filtering was not decoded"); + Fields linearMip = base; + linearMip.filterMode = 1; + rejectFields(linearMip, "min or max reduction with a linear mip filter"); + linearMip.xyMagFilter = 0; + linearMip.xyMinFilter = 0; + rejectFields(linearMip, "min or max reduction with a linear mip filter"); + + Fields badFilterMode = base; + badFilterMode.filterMode = 3; + rejectFields(badFilterMode, "unknown reduction filter mode 3"); + Fields anisoReduction = base; + anisoReduction.filterMode = 1; + anisoReduction.xyMagFilter = 3; + anisoReduction.xyMinFilter = 3; + anisoReduction.maxAnisoRatio = 2; + anisoReduction.mipFilter = 1; + rejectFields(anisoReduction, "min or max reduction with anisotropic filtering"); + anisoReduction.filterMode = 0; + Require(DecodeSamplerResource(pack(anisoReduction)).anisotropyEnable, "anisotropic weighted-average sampler was rejected"); + + auto compared = captured; + compared.compareEnable = true; + reject([&] { createSampler(context, compared); }, "min or max reduction with depth comparison"); + auto weightedCompare = DecodeSamplerResource(pack(base)); + weightedCompare.compareEnable = true; + Require(createSampler(context, weightedCompare).created, "weighted-average comparison sampler was rejected"); + context.samplerFilterMinmax = false; + reject([&] { createSampler(context, captured); }, "min or max reduction which the device does not support"); + Require(createSampler(context, DecodeSamplerResource(pack(base))).created, "weighted-average sampler needs min/max support"); + + context.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_R32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT; + if (format == VK_FORMAT_R32G32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + if (format == VK_FORMAT_R8G8B8A8_UNORM) properties->linearTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT; + }; + context.samplerFilterMinmax = true; + Fields minPoint = base; + minPoint.filterMode = 1; + minPoint.xyMagFilter = 0; + minPoint.xyMinFilter = 0; + minPoint.mipFilter = 1; + const std::array samplers{std::make_shared(context, DecodeSamplerResource(pack(base))), std::make_shared(context, captured), std::make_shared(context, DecodeSamplerResource(pack(minPoint)))}; + RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b010u, samplers); + RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0u, samplers); + RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b101u, samplers); + reject([&] { RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b010u, samplers); }, "format 37 does not support min/max filtering is sampled through a min or max reduction sampler"); + reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32G32_SFLOAT, 0b011u, samplers); }, "does not support min/max filtering"); + reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b1000u, samplers); }, "sampler element 3, which its shader does not bind"); + + const auto pointFiltered = [](const std::array& words) { return ShaderRecompiler::PointFilteredSamplerWord(words[0], words[2]); }; + Require(pointFiltered(pack(base)) == 0x05000000u, "a point-filtered weighted-average sampler kept its bilinear or linear mip filter"); + Require(pointFiltered(pack(minPoint)) == pack(minPoint)[2], "a point-filtered min sampler that already point-samples changed"); + reject([&] { pointFiltered(capturedSampler); }, "needs point filtering"); + Fields maxLinearMip = minPoint; + maxLinearMip.filterMode = 2; + maxLinearMip.mipFilter = 2; + reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering"); +} + } void RunGuestSamplerResourceTests() { @@ -239,9 +376,7 @@ void RunGuestSamplerResourceTests() { badCubeWrap.disableCubeWrap = true; rejectFields(badCubeWrap, "seamless cube filtering"); - Fields badFilterMode = base; - badFilterMode.filterMode = 1; - rejectFields(badFilterMode, "reduction filter mode"); + RunSamplerReductionTests(base); Fields badDegamma = base; badDegamma.disableDegamma = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 24da896c3..0adf42ad9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -31,7 +31,7 @@ void setEnvironment(const char* name, const std::string& value) { } bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { - return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized; + return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized && left.imageSamplers == right.imageSamplers; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -97,6 +97,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.bufferWritten = {false, false, true, true, false}; binding.samplerUnnormalized = {false, true, true}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; + binding.imageSamplers = {0x5u, 0u, 0x80000000u}; return binding; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index ac3b614f2..92443d6f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -38,6 +38,7 @@ constexpr std::uint32_t BorderTable = 3; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t ReductionMin = 1; constexpr std::array Red{0, 64, 128, 255}; alignas(256) std::array Input{}; @@ -53,6 +54,7 @@ struct Sampler { std::uint32_t clamp; std::uint32_t filter; std::uint32_t border = BorderBlack; + std::uint32_t reduction = 0; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -73,7 +75,7 @@ std::array TextureDescriptor(std::uint32_t format) { std::array SamplerDescriptor(const Sampler& sampler) { return { - sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u), + sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u) | (sampler.reduction << 29u), 0u, (sampler.filter << 20u) | (sampler.filter << 22u), sampler.border << 30u, @@ -214,6 +216,7 @@ int main() { Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); Reject(*device, Format8888UNorm, {ClampBorder, FilterPoint, BorderTable}, "border color table"); Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); + Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, ReductionMin}, "min or max reduction"); std::puts("emulated color compare tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp new file mode 100644 index 000000000..648a88e52 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -0,0 +1,319 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Levels = 2; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ReductionMin = 1; +constexpr std::uint32_t ReductionMax = 2; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t MipNone = 0; +constexpr std::uint32_t MipPoint = 1; +constexpr std::uint32_t MipLinear = 2; +constexpr float Margin = 0.05f; +constexpr std::array Level0{17, 11, 23, 14, 20, 25, 12, 19, 13, 22, 16, 24, 21, 10, 18, 15}; +constexpr std::array Level1{5, 30, 16.5f, 9}; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; +alignas(4096) std::array Colors{}; + +alignas(256) constexpr std::array Code{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadedCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0x36180083, 0x7e1a0280, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0000108, 0x0005050c, + 0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampledCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0900108, 0x00850501, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +struct Sampler { + std::uint32_t reduction; + std::uint32_t filter; + std::uint32_t mip; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(std::uint32_t format, const void* texels = Texels.data(), std::uint32_t levels = Levels) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((levels - 1u) << 16u) | (Type2D << 28u), + 0u, + (levels - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(const Sampler& sampler) { + return {0x92u | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u}; +} + +void FillTexels() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format32Float, Size, Size, Levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample reduction: the mip chain does not fit the texel storage"); + Texels.fill(0); + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = mips[level]; + const auto& values = level == 0 ? std::span(Level0) : std::span(Level1); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto bits = std::bit_cast(values[y * mip.width + x]); + auto* texel = &Texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + for (std::uint32_t byte = 0; byte < 4u; ++byte) texel[byte] = static_cast(bits >> (byte * 8u)); + } + } + } +} + +std::uint8_t Red(int x, int y) { + const auto cx = static_cast(std::clamp(x, 0, static_cast(Size) - 1)); + const auto cy = static_cast(std::clamp(y, 0, static_cast(Size) - 1)); + return static_cast(17u * ((cy * 7u + cx * 5u) % 16u)); +} + +void FillColors() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Colors.size(), "image sample reduction: the 8_8_8_8 texture does not fit its storage"); + Colors.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Colors[mips[0].tiledOffset + static_cast(y) * mips[0].pitchBytes + x * 4u]; + texel[0] = Red(static_cast(x), static_cast(y)); + texel[3] = 255; + } + } +} + +bool Safe(float coordinate) { + for (std::uint32_t size = Size; size >= Size / 2; size /= 2) { + for (const float offset : {0.0f, -0.5f}) { + const float scaled = coordinate * static_cast(size) + offset; + const float fraction = scaled - std::floor(scaled); + if (fraction < Margin || fraction > 1.0f - Margin) return false; + } + } + return true; +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float u = (static_cast((tid * 5u + 1u) % 12u) - 1.7f) / 8.0f; + const float v = (static_cast((tid * 7u + 3u) % 12u) - 1.7f) / 8.0f; + Require(Safe(u) && Safe(v), "image sample reduction: thread " + std::to_string(tid) + " samples too close to a texel boundary"); + Input[tid * 3u + 0u] = u; + Input[tid * 3u + 1u] = v; + Input[tid * 3u + 2u] = tid % 2u == 0u ? 0.25f : 0.75f; + } +} + +float Texel(std::uint32_t level, int x, int y) { + const int size = static_cast(Size >> level); + const auto cx = static_cast(std::clamp(x, 0, size - 1)); + const auto cy = static_cast(std::clamp(y, 0, size - 1)); + return level == 0 ? Level0[cy * Size + cx] : Level1[cy * (Size / 2) + cx]; +} + +float Expected(std::uint32_t tid, const Sampler& sampler) { + const float u = Input[tid * 3u + 0u]; + const float v = Input[tid * 3u + 1u]; + const float lod = Input[tid * 3u + 2u]; + std::vector footprint; + const auto gather = [&](std::uint32_t level) { + const float size = static_cast(Size >> level); + if (sampler.filter == FilterPoint) { + footprint.push_back(Texel(level, static_cast(std::floor(u * size)), static_cast(std::floor(v * size)))); + return; + } + const int x = static_cast(std::floor(u * size - 0.5f)); + const int y = static_cast(std::floor(v * size - 0.5f)); + for (int dy = 0; dy < 2; ++dy) { + for (int dx = 0; dx < 2; ++dx) footprint.push_back(Texel(level, x + dx, y + dy)); + } + }; + gather(sampler.mip == MipPoint && lod > 0.5f ? 1u : 0u); + return sampler.reduction == ReductionMin ? *std::min_element(footprint.begin(), footprint.end()) : *std::max_element(footprint.begin(), footprint.end()); +} + +std::array ExpectedGather(std::uint32_t tid) { + const int x = static_cast(std::floor(Input[tid * 3u + 0u] * static_cast(Size) - 0.5f)); + const int y = static_cast(std::floor(Input[tid * 3u + 1u] * static_cast(Size) - 0.5f)); + return {Texel(0, x, y + 1), Texel(0, x + 1, y + 1), Texel(0, x + 1, y), Texel(0, x, y)}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span code = Code) { + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(format); + const auto samplerWords = SamplerDescriptor(sampler); + const auto colors = TextureDescriptor(Format8888UNorm, Colors.data(), 1); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(samplerWords.begin(), samplerWords.end(), userData.begin() + 16); + std::copy(colors.begin(), colors.end(), userData.begin() + 20); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) { + Output.fill(-1.0f); + const auto result = Compile(device, Format32Float, sampler); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float expected = Expected(tid, sampler); + Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +void Gather(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) { + Output.fill(-1.0f); + const auto result = Compile(device, Format32Float, sampler, GatherCode); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(GatherCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = ExpectedGather(tid); + for (std::uint32_t component = 0; component < 4u; ++component) { + const float sampled = Output[tid * 4u + component]; + Require(sampled == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(sampled) + ", expected " + std::to_string(expected[component])); + } + } +} + +std::uint32_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, std::uint32_t format) { + for (const auto& binding : result.bindings) { + if (binding.kind != ShaderRecompiler::DescriptorKind::SampledImage) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + if (((binding.guestDescriptor.at(element * 8u + 1u) >> 20u) & 0x1ffu) == format) return binding.imageSamplers.at(element); + } + } + Require(false, "image sample reduction: no sampled image of format " + std::to_string(format)); + return 0; +} + +float ExpectedColor(std::uint32_t tid, bool sampled) { + if (!sampled) return static_cast(Red(static_cast(tid & 3u), 0)) / 255.0f; + const int x = static_cast(std::floor(Input[tid * 3u + 0u] * static_cast(Size) - 0.5f)); + const int y = static_cast(std::floor(Input[tid * 3u + 1u] * static_cast(Size) - 0.5f)); + return static_cast(std::min({Red(x, y), Red(x + 1, y), Red(x, y + 1), Red(x + 1, y + 1)})) / 255.0f; +} + +void RunWithColors(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& result, std::span code, bool sampled, const char* name) { + const Sampler sampler{ReductionMin, FilterBilinear, MipPoint}; + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float expected = Expected(tid, sampler); + Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + " from 32_FLOAT, expected " + std::to_string(expected)); + const float color = ExpectedColor(tid, sampled); + Require(std::fabs(Output[Threads + tid] - color) <= 1.0f / 1024.0f, std::string(name) + ": thread " + std::to_string(tid) + " read " + std::to_string(Output[Threads + tid]) + " from 8_8_8_8, expected " + std::to_string(color)); + } +} + +void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::string_view reason) { + try { + static_cast(Compile(device, format, sampler)); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected rejection: ") + error.what()); + return; + } + Require(false, std::string("expected rejection: ") + std::string(reason)); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillColors(); + FillInput(); + const Sampler minBilinear{ReductionMin, FilterBilinear, MipPoint}; + const auto loaded = Compile(*device, Format32Float, minBilinear, LoadedCode); + Require(PairedSamplers(loaded, Format32Float) == 1u && PairedSamplers(loaded, Format8888UNorm) == 0u, "image_load of 8_8_8_8 next to a min sampler: wrong image-sampler pairs"); + const auto sampled = Compile(*device, Format32Float, minBilinear, SampledCode); + Require(PairedSamplers(sampled, Format32Float) == 1u && PairedSamplers(sampled, Format8888UNorm) == 1u, "8_8_8_8 sampled through a min sampler: wrong image-sampler pairs"); + static_cast(Compile(*device, Format32SInt, {ReductionMin, FilterPoint, MipPoint})); + Reject(*device, Format32SInt, {ReductionMin, FilterBilinear, MipPoint}, "needs point filtering"); + Reject(*device, Format32SInt, {ReductionMax, FilterPoint, MipLinear}, "needs point filtering"); + if (!device->SamplerFilterMinmax()) { + std::puts("skipped, the device has no min/max sampler reduction with component mapping"); + return VulkanTestSkipped; + } + Run(*device, {ReductionMin, FilterBilinear, MipPoint}, "min, bilinear, point mip"); + Run(*device, {ReductionMax, FilterBilinear, MipPoint}, "max, bilinear, point mip"); + Run(*device, {ReductionMin, FilterPoint, MipPoint}, "min, point, point mip"); + Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip"); + Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip"); + Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip"); + Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min"); + Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max"); + RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8"); + try { + RunWithColors(*device, sampled, SampledCode, true, "min 32_FLOAT and 8_8_8_8"); + std::puts("the device filters 8_8_8_8 UNORM with min/max reduction"); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("format 37 does not support min/max filtering is sampled through a min or max reduction sampler") != std::string_view::npos, std::string("min 32_FLOAT and 8_8_8_8: unexpected error: ") + error.what()); + } + std::puts("image sample reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index bac6a047a..cae8055d8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -4,9 +4,12 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/BindingAllocator.hpp" #include "Recompiler.hpp" +#include namespace ShaderRecompiler { +std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter); + class DescriptorBindingBuilder { public: void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index d88d1cb35..6791a87b8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -240,6 +240,34 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh return proof; } +std::vector SamplerElements(const IrBindingLayout& layout, const ShaderInfo& info) { + std::vector elements(info.samplers.size(), ShaderInfo::MaxSamplers); + for (const IrDescriptorBinding& logical : layout.descriptors) { + if (logical.kind != DescriptorBindingKind::Samplers) { + continue; + } + for (std::uint32_t element = 0; element < logical.resources.size() && element < ShaderInfo::MaxSamplers; element++) { + elements.at(logical.resources[element]) = element; + } + } + return elements; +} + +std::uint32_t ImageSamplerMask(const ShaderInfo& info, const std::vector& samplerElements, std::uint32_t resource) { + const std::uint32_t root = info.images.at(resource).indirectRoot; + std::uint32_t mask = 0; + for (const SampledResourcePair& pair : info.sampledPairs) { + if (pair.image != resource && pair.image != root) { + continue; + } + if (pair.sampler >= samplerElements.size() || samplerElements[pair.sampler] >= ShaderInfo::MaxSamplers) { + fail("DescriptorBindingBuilder::Populate sampled image pair names a sampler outside the first " + std::to_string(ShaderInfo::MaxSamplers) + " elements of the sampler binding"); + } + mask |= 1u << samplerElements[pair.sampler]; + } + return mask; +} + std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) { @@ -260,6 +288,15 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st } +std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter) { + const bool reduced = ((word0 >> 29u) & 3u) != 0u; + if (reduced && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u)) { + fail("DescriptorBindingBuilder: a min or max reduction sampler that filters between texels or mip levels samples an image that needs point filtering (sint, converted or depth-bits format), which is not implemented"); + } + const bool mipmapped = ((filter >> 26u) & 3u) != 0u; + return (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); +} + void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot, partialThreads); } @@ -268,6 +305,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con const IrBindingLayout& layout = allocation.layout; const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); const UnnormalizedProof unnormalized = ProveUnnormalized(info, snapshot); + const std::vector samplerElements = SamplerElements(layout, info); std::vector bindings; bindings.reserve(layout.descriptors.size()); @@ -305,6 +343,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageDepthCompare.push_back(image.depthCompare); physical.imageAtomic.push_back(image.atomic); physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); + physical.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource)); } break; case DescriptorRole::GuestSamplers: @@ -315,8 +354,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.samplerUnnormalized.push_back(unnormalized.samplers.at(logical.resources[element])); if (sampler.forcePointFiltering) { auto& filter = physical.guestDescriptor.at(element * 4u + 2u); - const bool mipmapped = ((filter >> 26u) & 3u) != 0u; - filter = (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); + filter = PointFilteredSamplerWord(physical.guestDescriptor.at(element * 4u), filter); } } break; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index f88ed74a3..fcb675049 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -536,6 +536,7 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna const auto clampY = (words[0] >> 3u) & 0x7u; const auto function = (words[0] >> 12u) & 0x7u; const bool unnormalized = ((words[0] >> 15u) & 0x1u) != 0u; + if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto lodBias = words[2] & 0x3fffu; const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e3d69389d..8d8b10416 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -330,6 +330,7 @@ struct DescriptorBinding { std::vector bufferWritten; std::vector samplerUnnormalized; std::vector imageUnnormalized; + std::vector imageSamplers; }; struct VertexAttribute { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index d060071fd..0f9f0d0dc 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,7 +64,7 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(RecompileResult) == 176, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 384, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 408, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 28, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder"); @@ -253,6 +253,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.bufferWritten); writer.Flags(binding.samplerUnnormalized); writer.Flags(binding.imageUnnormalized); + writer.Values(std::span(binding.imageSamplers)); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { @@ -274,6 +275,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.bufferWritten); reader.Flags(binding.samplerUnnormalized); reader.Flags(binding.imageUnnormalized); + reader.Values(binding.imageSamplers); } void encodeResult(Writer& writer, const RecompileResult& result) { diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 3abc5c257..a0af13f93 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 5; +inline constexpr std::uint32_t FormatVersion = 6; enum class LoadStatus { Loaded, diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1d7e36087..b0eb65037 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -222,6 +222,8 @@ Throughout the project, every function at every stage either **does exactly what - [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. +- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction, assumed to reduce the same texels as the console's filter (those of non-zero weight). +- [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped From 1c5ed6594d7ea6fc510aa3e4c83499e4f1c78b48 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:12:15 +0200 Subject: [PATCH 170/203] feat(shader): accept more than 32 buffers per shader up to the device limit The resource tracker refused a program's 33rd distinct V# with "buffer resource limit exceeded". The 32 was a recompiler constant, not a Vulkan limit: every buffer is one constant-indexed element of the guest buffers binding, which ShaderResources checks against the device's storage-buffer limits. Drop ShaderInfo::MaxBuffers and the tracker's refusal, and size the SPIR-V emitter's per-buffer offset list from the binding layout. When the user data and offsets do not fit in the push constants, they all go to the shader data buffer as before. ShaderResources counted only storage buffers against maxPerStageResources (200 on Intel UHD) and now adds the stage's sampled and storage images. agc_driver_many_buffers reads and writes 52 and 124 buffers, and agc_driver_graphics rejects buffers plus an image above the limit. --- .../Graphics/src/ShaderResources.cpp | 10 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 21 +++ .../tests/execution/ManyBuffers.cpp | 156 ++++++++++++++++++ .../IrMetadata/ShaderInfo.hpp | 1 - .../Optimization/src/ResourceTracker.cpp | 6 - .../SpirvBackend/SpirvEmitterState.hpp | 2 +- .../src/SpirvMemory/SpirvBufferAccess.cpp | 4 +- docs/dev/TechnicalDebt.md | 1 + 8 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 759380e79..d46308af4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -896,7 +896,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); const VkShaderStageFlags flags = VulkanStage(shader.stage); - std::uint64_t stageDescriptors = 0; + std::uint64_t stageStorageBuffers = 0; + std::uint64_t stageResources = 0; std::vector offsetsInData; std::int64_t shaderData = -1; for (const auto& binding : shader.program->bindings) { @@ -906,6 +907,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt; const bool imageRole = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; if (imageRole) { + if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage || binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) stageResources += binding.count; + Require(stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); addImageBinding(binding, flags); continue; } @@ -913,9 +916,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (binding.kind != ShaderRecompiler::DescriptorKind::StorageBuffer) Require(false, std::string("unsupported descriptor kind ") + kindName(binding.kind) + " for role " + roleName(binding.role) + ": only StorageBuffer is supported"); Require(!binding.readOnly, "read-only descriptors are unsupported because the recompiler emits no NonWritable decoration"); Require(binding.count != 0, "empty descriptor binding"); - stageDescriptors += binding.count; + stageStorageBuffers += binding.count; + stageResources += binding.count; storageBuffers += binding.count; - Require(stageDescriptors <= context.limits.maxPerStageDescriptorStorageBuffers && stageDescriptors <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); + Require(stageStorageBuffers <= context.limits.maxPerStageDescriptorStorageBuffers && stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits"); Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, binding.count, flags, nullptr}, {}}; if (binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9be7d4dce..f5f255234 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1275,6 +1275,27 @@ void resourceTests() { expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); + { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)))); + vertex.bindings.push_back(makeBinding(Role::GuestImages, 1, 1, std::vector(8, 0))); + const auto expectStageResources = [&](Role role, Kind kind, std::uint32_t words, std::uint32_t limit, std::string_view reason) { + vertex.bindings.back().role = role; + vertex.bindings.back().kind = kind; + vertex.bindings.back().guestDescriptor.assign(words, 0); + mock = MockVulkan{}; + auto limited = mockContext(); + limited.limits.maxPerStageResources = limit; + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, vertex, fragment, state.color, 0, 0); }, reason); + Require(mock.live == 0, "failed shader resources leaked Vulkan objects"); + }; + expectStageResources(Role::GuestImages, Kind::SampledImage, 8, 2, "shader descriptors exceed per-stage limits"); + expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 2, "shader descriptors exceed per-stage limits"); + expectStageResources(Role::GuestImages, Kind::SampledImage, 8, 3, "missing an image shape"); + expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 3, "detiler is unavailable"); + expectStageResources(Role::GuestSamplers, Kind::Sampler, 4, 2, "shader sampler descriptors exceed per-stage limits"); + } { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp new file mode 100644 index 000000000..3a26a3ca1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp @@ -0,0 +1,156 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::DescriptorRole; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t PushBuffers = 52; +constexpr std::uint32_t DataBuffers = 124; +constexpr std::uint32_t MaxBuffers = DataBuffers; +constexpr std::uint32_t RegionWords = 2u * Threads + 4u; +constexpr std::uint32_t RangeBytes = (RegionWords + 4u) * 4u; +constexpr std::uint32_t TableRegister = 0; +constexpr std::uint32_t DescriptorRegister = 4; +constexpr std::uint32_t NullRegister = 0x7du; +constexpr std::uint32_t ThreadVector = 0; +constexpr std::uint32_t OffsetVector = 1; +constexpr std::uint32_t DataVector = 2; +constexpr std::uint32_t ConstantZero = 0x80u; +constexpr std::uint32_t ConstantTwo = 0x82u; +constexpr std::uint32_t Literal = 0xffu; +constexpr std::uint32_t WaitScalarLoads = 0xbf8cc07fu; +constexpr std::uint32_t WaitVectorLoads = 0xbf8c3f70u; +constexpr std::uint32_t Endpgm = 0xbf810000u; + +alignas(4096) std::array Data{}; +alignas(256) std::array Table{}; + +constexpr std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t destination, std::uint32_t source0, std::uint32_t source1) { + return (opcode << 25u) | (destination << 17u) | (source1 << 9u) | source0; +} + +constexpr std::uint32_t Increment(std::uint32_t buffer) { + return (buffer + 1u) * 0x01000193u; +} + +constexpr std::size_t CodeWords(std::uint32_t buffers) { + return 2u + buffers * 10u; +} + +template +constexpr std::array BuildCode() { + constexpr std::uint32_t vLshlrevB32 = 0x1au; + constexpr std::uint32_t vAddNcU32 = 0x25u; + constexpr std::uint32_t sLoadDwordx4 = 0xf4080000u | (DescriptorRegister << 6u) | (TableRegister / 2u); + constexpr std::uint32_t bufferLoadDword = 0xe0301000u; + constexpr std::uint32_t bufferStoreDword = 0xe0701000u | (Threads * 4u); + constexpr std::uint32_t operands = (ConstantZero << 24u) | ((DescriptorRegister / 4u) << 16u) | (DataVector << 8u) | OffsetVector; + std::array code{}; + std::size_t count = 0; + code[count++] = Vop2(vLshlrevB32, OffsetVector, ConstantTwo, ThreadVector); + for (std::uint32_t buffer = 0; buffer < Buffers; ++buffer) { + code[count++] = sLoadDwordx4; + code[count++] = (NullRegister << 25u) | (buffer * 16u); + code[count++] = WaitScalarLoads; + code[count++] = bufferLoadDword; + code[count++] = operands; + code[count++] = WaitVectorLoads; + code[count++] = Vop2(vAddNcU32, DataVector, Literal, DataVector); + code[count++] = Increment(buffer); + code[count++] = bufferStoreDword; + code[count++] = operands; + } + code[count++] = Endpgm; + return code; +} + +alignas(256) constexpr std::array PushCode = BuildCode(); +alignas(256) constexpr std::array DataCode = BuildCode(); + +std::uint32_t BufferBase(std::uint32_t buffer) { + return buffer * RegionWords + buffer % 4u; +} + +std::uint32_t InputValue(std::uint32_t buffer, std::uint32_t tid) { + return 0x40000000u | (buffer << 8u) | tid; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t buffers, bool shaderData) { + const std::string label = "many buffers (" + std::to_string(buffers) + "): "; + Data.fill(0xdeadbeefu); + for (std::uint32_t buffer = 0; buffer < buffers; ++buffer) { + const auto base = BufferBase(buffer); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Data[base + tid] = InputValue(buffer, tid); + const auto address = reinterpret_cast(&Data[base]); + Table[buffer * 4u + 0u] = static_cast(address); + Table[buffer * 4u + 1u] = static_cast((address >> 32u) & 0xffffu); + Table[buffer * 4u + 2u] = RangeBytes; + Table[buffer * 4u + 3u] = 0x01016facu; + } + const auto table = reinterpret_cast(Table.data()); + const std::vector userData{static_cast(table), static_cast(table >> 32u)}; + const std::span tableWords(Table.data(), buffers * 4u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {table, std::as_bytes(tableWords)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto guest = std::find_if(result.bindings.begin(), result.bindings.end(), [](const auto& binding) { return binding.role == DescriptorRole::GuestBuffers; }); + Require(guest != result.bindings.end() && guest->count == buffers, label + "the program does not bind every buffer"); + const bool hasShaderData = std::any_of(result.bindings.begin(), result.bindings.end(), [](const auto& binding) { return binding.role == DescriptorRole::ShaderData; }); + Require(hasShaderData == shaderData, label + (shaderData ? "the buffer offsets are not in shader data" : "the buffer offsets are not in push constants")); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t buffer = 0; buffer < buffers; ++buffer) { + const auto base = BufferBase(buffer); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto input = Data[base + tid]; + const auto actual = Data[base + Threads + tid]; + const auto expected = InputValue(buffer, tid) + Increment(buffer); + Require(input == InputValue(buffer, tid), label + "buffer " + std::to_string(buffer) + " thread " + std::to_string(tid) + " input changed to " + Hex(input)); + Require(actual == expected, label + "buffer " + std::to_string(buffer) + " thread " + std::to_string(tid) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, PushCode, PushBuffers, false); + Run(*device, DataCode, DataBuffers, true); + std::puts("many buffers tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 23fe56c39..aa6ee6b96 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -12,7 +12,6 @@ namespace ShaderRecompiler { struct ShaderInfo { std::uint32_t scratchDwords = 0; std::uint32_t sharedMemoryBytes = 0; - static constexpr std::uint32_t MaxBuffers = 32; static constexpr std::uint32_t MaxImages = 64; static constexpr std::uint32_t MaxSamplers = 32; static constexpr std::uint32_t MaxSampledPairs = 64; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index bcd33a91c..2c4e77ab3 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -831,9 +831,6 @@ private: return i; } } - if (m_info.buffers.size() >= ShaderInfo::MaxBuffers) { - return std::numeric_limits::max(); - } BufferResource resource; resource.source = source; resource.firstUsePc = pc; @@ -982,9 +979,6 @@ private: } GetHandle(inst.Argument(0), IrOpcode::GetBufferResource, 4, handle, source); resource = AddBuffer(source, memory, op, flags.pc); - if (resource == std::numeric_limits::max()) { - fail("buffer resource limit exceeded"); - } AddHandlePatch(handle, resource); AddMemoryPatch(flags.index, resource, 0, false); return; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1e761cf73..8d784ddc2 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -96,7 +96,7 @@ struct SpirvEmitterState { bool tableIndexNonUniform = true; std::uint32_t storageBufferVariable = 0; std::uint32_t storageBufferU64Variable = 0; - std::array memoryByteOffsets {}; + std::vector memoryByteOffsets; std::uint32_t bdaPagetableVariable = 0; std::uint32_t faultBufferVariable = 0; std::uint32_t bdaPointerFunction = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 9c7ee7228..2b01104f6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -82,9 +82,7 @@ IrBufferFormat StorageBufferFormat(const SpirvEmitterState& state, const MemoryI void EmitMemoryOffsets(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; - if (layout.memoryOffsetCount > state.memoryByteOffsets.size()) { - FailEmit("memory offset count exceeds the buffer limit"); - } + state.memoryByteOffsets.assign(layout.memoryOffsetCount, 0u); for (std::uint32_t i = 0; i < layout.memoryOffsetCount; i++) { const auto word = EmitShaderDataDwordLoad(state, layout.memoryOffsetDword + i / 4u); const auto shift = ConstantU32(state, (i % 4u) * 8u); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1d7e36087..561fee1e3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -202,6 +202,7 @@ Throughout the project, every function at every stage either **does exactly what - [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. +- [Per-stage descriptor count](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a pixel shader's colour attachments are not counted against `maxPerStageResources`, so its buffers and images can fit the limit while the pipeline exceeds it - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink. - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. From 921335571123d53060dc2a7f6bc7637cd25856ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 09:22:02 +0200 Subject: [PATCH 171/203] docs(TechnicalDebt): record measured min/max reduction footprint Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b0eb65037..c4ee68aa9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -222,7 +222,7 @@ Throughout the project, every function at every stage either **does exactly what - [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. -- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction, assumed to reduce the same texels as the console's filter (those of non-zero weight). +- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. From ac9287c2ef13b9b3336fdb3669fa5a65e5479b88 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Wed, 7 Oct 2026 09:59:05 +0200 Subject: [PATCH 172/203] refactor(recompiler): share the stage input info helpers StageOf, VertexInfo, PixelInfo and ShaderWorkgroupInput were defined in the anonymous namespaces of up to three SPIR-V backend sources with identical bodies; they are now declared next to FailEmit. Co-Authored-By: Claude Opus 5.5 --- .../SpirvBackend/SpirvEmitterHelpers.hpp | 4 ++ .../src/SpirvMemory/SpirvBufferAccess.cpp | 17 -------- .../src/SpirvMemory/SpirvDescriptors.cpp | 4 -- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 30 +++++++++++++- .../src/SpirvMemory/SpirvInputOutput.cpp | 18 --------- .../src/SpirvMemory/SpirvModuleSetup.cpp | 39 ------------------- 6 files changed, 33 insertions(+), 79 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index caec0c7bb..7d8f0874c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -8,6 +8,10 @@ namespace ShaderRecompiler { [[noreturn]] void FailEmit(const std::string& reason); +IrShaderStage StageOf(const SpirvEmitterState& state); +const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state); +const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state); +const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index b63540f83..e851e0d13 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -12,23 +12,6 @@ namespace ShaderRecompiler { namespace { - const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { - switch (state.program.Resources().stage) { - case IrShaderStage::Compute: - if (state.inputInfo.compute == nullptr) { - FailEmit("compute input info is missing"); - } - return state.inputInfo.compute; - case IrShaderStage::Mesh: - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return &state.inputInfo.vertex->mesh; - default: - return nullptr; - } - } - void EnsureLdsStorage(SpirvEmitterState& state) { if (state.ldsVariable != 0) { return; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 0fe12f8ef..a9de20d7d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -12,10 +12,6 @@ namespace ShaderRecompiler { namespace { -IrShaderStage StageOf(const SpirvEmitterState& state) { - return state.program.Resources().stage; -} - constexpr RdnaImageDimensionInfo ImageDimensions[] = { {RdnaImageDimension::Dim1D, spv::Dim1D, 1, 1, 0, 0}, {RdnaImageDimension::Dim2D, spv::Dim2D, 2, 2, 0, 0}, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 0edb9069a..28a7bdf07 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -16,7 +16,16 @@ void FailEmit(const std::string& reason) { throw std::runtime_error("SPIR-V module emission failed: " + reason); } -namespace { +IrShaderStage StageOf(const SpirvEmitterState& state) { + return state.program.Resources().stage; +} + +const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state) { + if (state.inputInfo.vertex == nullptr) { + FailEmit("vertex input info is missing"); + } + return *state.inputInfo.vertex; +} const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { if (state.inputInfo.pixel == nullptr) { @@ -25,6 +34,25 @@ const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { return *state.inputInfo.pixel; } +const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { + switch (state.program.Resources().stage) { + case IrShaderStage::Compute: + if (state.inputInfo.compute == nullptr) { + FailEmit("compute input info is missing"); + } + return state.inputInfo.compute; + case IrShaderStage::Mesh: + if (state.inputInfo.vertex == nullptr) { + FailEmit("vertex input info is missing"); + } + return &state.inputInfo.vertex->mesh; + default: + return nullptr; + } +} + +namespace { + constexpr std::uint32_t NoBuiltIn = std::numeric_limits::max(); std::uint32_t BuiltInForInput(StageInputKind kind) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index a4e551735..72f796336 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -19,24 +19,6 @@ namespace ShaderRecompiler constexpr std::uint32_t PsInputFlatShade = 0x00000400u; constexpr std::uint32_t PixelParameterLimit = 32u; - IrShaderStage StageOf(const SpirvEmitterState& state) { - return state.program.Resources().stage; - } - - const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state) { - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return *state.inputInfo.vertex; - } - - const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { - if (state.inputInfo.pixel == nullptr) { - FailEmit("pixel input info is missing"); - } - return *state.inputInfo.pixel; - } - bool IsVertexLikeStage(const SpirvEmitterState& state) { return StageOf(state) == IrShaderStage::Vertex || StageOf(state) == IrShaderStage::Local; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9b42dde31..e17e8445b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -10,45 +10,6 @@ namespace ShaderRecompiler { -namespace { - -IrShaderStage StageOf(const SpirvEmitterState& state) { - return state.program.Resources().stage; -} - -const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state) { - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return *state.inputInfo.vertex; -} - -const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { - if (state.inputInfo.pixel == nullptr) { - FailEmit("pixel input info is missing"); - } - return *state.inputInfo.pixel; -} - -const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { - switch (state.program.Resources().stage) { - case IrShaderStage::Compute: - if (state.inputInfo.compute == nullptr) { - FailEmit("compute input info is missing"); - } - return state.inputInfo.compute; - case IrShaderStage::Mesh: - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return &state.inputInfo.vertex->mesh; - default: - return nullptr; - } -} - -} - void EmitModuleHeader(SpirvModule& module, const IrProgram& program, const BindingAllocationResult& bindings) { CheckBindings(program, bindings); From 048d9982cda337c61150c635e4b5a71620018cbf Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Wed, 7 Oct 2026 10:09:51 +0200 Subject: [PATCH 173/203] fix(agc): the Z-format precheck gives no verdict for a missing SPI_SHADER_Z_FORMAT DrawRejection read 0x1c4 with read(), which throws when the register is absent, against the function's rule that a missing register gives no verdict there; it now reuses the value read with value() for the DB_SHADER_CONTROL check. Tests pin the Z-export rules: formats 1, 2, 3 and 9 with Z_EXPORT_ENABLE accepted, format 0 rejected, the sample-mask export accepted with 32_ABGR and rejected with 32_R, formats 4 to 8 rejected, and a missing 0x1c4 accepted. --- .../libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c2c508b37..18feef999 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -790,7 +790,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word); - if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(zFormat) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue); for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) { if (find(cx, offset) != cx.end()) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3cd226c5a..dca4b6f5b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -573,6 +573,35 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets"); } +// SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a +// format with a depth channel (1, 2, 3 or 32_ABGR 9), the sample mask needs 32_ABGR, and the +// formats the export path does not lay out are refused. A missing 0x1c4 gives no verdict here. +void ZExportTests() { + constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t maskExportEnable = 0x100u; + const auto withExports = [](std::uint32_t format, std::uint32_t exports) { + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x1c4] = format; + queue.context[0x203] = 0x800u | exports; + return queue; + }; + for (const std::uint32_t format : {1u, 2u, 3u, 9u}) { + const auto queue = withExports(format, zExportEnable); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a Z export with format " + std::to_string(format) + " was rejected"); + } + Require(!AgcDriver::Graphics::DrawRejection(withExports(0, zExportEnable), true).empty(), "a Z export without a Z format was accepted"); + Require(AgcDriver::Graphics::DrawRejection(withExports(9, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_ABGR was rejected"); + Require(!AgcDriver::Graphics::DrawRejection(withExports(1, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_R was accepted"); + for (std::uint32_t format = 4; format <= 8; ++format) { + Require(!AgcDriver::Graphics::DrawRejection(withExports(format, 0), true).empty(), "Z format " + std::to_string(format) + " was accepted"); + } + auto absent = withExports(0, 0); + absent.context.erase(0x1c4); + Require(AgcDriver::Graphics::DrawRejection(absent, true).empty(), "a missing SPI_SHADER_Z_FORMAT was rejected (or threw) in the precheck"); +} + void DepthBoundsBiasTests() { const auto bits = [](float value) { std::uint32_t word = 0; @@ -1907,6 +1936,7 @@ int main() { hardwareScreenOffsetTests(); DepthClipTests(); DepthStencilTests(); + ZExportTests(); DepthBoundsBiasTests(); DisabledColorTests(); CompactedExportTests(); From 9df592926bb39d8708f93ab7840e6a1914c1d138 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 6 Oct 2026 15:53:38 +0200 Subject: [PATCH 174/203] feat(recompiler): implement 64-bit image atomics image_atomic_* with a two-dword data mask (four for cmpswap) is the 64-bit form of an integer image atomic. The decoder rejected it with "invalid MIMG data mask". PPSA14632 (Unreal Engine) uses image_atomic_umax this way in a compute shader during its first frames. Swap, add, sub, signed and unsigned min/max, and, or, xor and cmpswap now decode with 64-bit data and translate to new ImageAtomic*64 IR opcodes. An image updated by them gets its own descriptor binding range, declared as an R64ui image of 64-bit integers, and the module uses Int64ImageEXT from SPV_EXT_shader_image_int64. The driver enables shaderImageInt64Atomics when the device offers it and binds an R64_UINT view of the 32_32 UINT, SINT or FLOAT surface. Without the feature, recompiling such a shader throws. 64-bit inc, dec and the float atomics still throw, as does any other access in the same shader to an image it updates with 64-bit atomics. The new agc_driver_image_atomics64 test runs every operation on 32_32 UINT, SINT and FLOAT surfaces and checks the returned and stored values against edge cases across the dword boundary. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 17 ++ .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 3 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 10 + .../tests/execution/ImageAtomics64.cpp | 189 ++++++++++++++++++ .../IrMetadata/DescriptorBinding.hpp | 2 +- .../IrMetadata/Resources.hpp | 1 + .../IntermediateRepresentation/IrOpcode.hpp | 12 ++ .../src/IrOpcode.cpp | 26 +++ .../src/IrProgram.cpp | 3 +- .../src/DescriptorBindingBuilder.cpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 5 +- .../Optimization/src/ResourceTracker.cpp | 5 + .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 6 +- core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 6 +- .../include/SpirvBackend/SpirvAnalysis.hpp | 1 + .../SpirvBackend/SpirvEmitterInstructions.hpp | 11 + .../SpirvBackend/src/SpirvAnalysis.cpp | 3 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 11 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 86 +++++++- .../src/SpirvMemory/SpirvDescriptors.cpp | 5 +- .../src/SpirvMemory/SpirvModuleSetup.cpp | 9 + .../MemoryInstructions/ImageInstructions.cpp | 32 ++- docs/dev/TechnicalDebt.md | 1 + 26 files changed, 436 insertions(+), 12 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 3d848fde1..c17cfcdf5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -748,6 +748,19 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u atomicInt64Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES_KHR}; atomicInt64Features.shaderBufferInt64Atomics = VK_TRUE; if (bufferInt64Atomics) deviceExtensions.push_back(VK_KHR_SHADER_ATOMIC_INT64_EXTENSION_NAME); + VkPhysicalDeviceShaderImageAtomicInt64FeaturesEXT imageAtomicInt64Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_ATOMIC_INT64_FEATURES_EXT}; + if (hasExtension(VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &imageAtomicInt64Features}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + } + const bool imageInt64Atomics = imageAtomicInt64Features.shaderImageInt64Atomics == VK_TRUE; + imageAtomicInt64Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_ATOMIC_INT64_FEATURES_EXT}; + imageAtomicInt64Features.shaderImageInt64Atomics = VK_TRUE; + if (imageInt64Atomics) { + deviceExtensions.push_back(VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityInt64ImageEXT); + state->spirvExtensions.push_back("SPV_EXT_shader_image_int64"); + } deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve); state->spirvExtensions.push_back("SPV_KHR_float_controls"); @@ -940,6 +953,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u atomicInt64Features.pNext = byteFeatures.pNext; byteFeatures.pNext = &atomicInt64Features; } + if (imageInt64Atomics) { + imageAtomicInt64Features.pNext = byteFeatures.pNext; + byteFeatures.pNext = &imageAtomicInt64Features; + } VkPhysicalDeviceImageRobustnessFeaturesEXT imageRobustnessFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES_EXT, nullptr, VK_TRUE}; if (imageRobustness) { imageRobustnessFeatures.pNext = byteFeatures.pNext; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 9df5a3c1b..29e48af83 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -412,6 +412,7 @@ private: std::vector storageFirstLayer; std::vector storageWritten; std::vector storageAtomic; + std::vector storageAtomic64; std::vector> samplers; bool reusable = false; std::vector directRegions; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 10fb690a3..ee2c5042d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -125,6 +125,7 @@ public: VkImageView View(std::uint32_t mip); VkImageView FirstLayerView(std::uint32_t mip); VkImageView AtomicView(std::uint32_t mip, bool firstLayer); + VkImageView Atomic64View(std::uint32_t mip, bool firstLayer); // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 5f4c021ab..86ff4ab3e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1068,7 +1068,7 @@ void ShaderResources::buildComplete() { break; case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: write.pImageInfo = images.data() + images.size(); - for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageFirstLayer[index] ? storageTextures[index]->FirstLayerView(storageMips[index]) : storageTextures[index]->View(storageMips[index]), VK_IMAGE_LAYOUT_GENERAL}); + for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageFirstLayer[index] ? storageTextures[index]->FirstLayerView(storageMips[index]) : storageTextures[index]->View(storageMips[index]), VK_IMAGE_LAYOUT_GENERAL}); break; case VK_DESCRIPTOR_TYPE_SAMPLER: write.pImageInfo = images.data() + images.size(); @@ -2591,6 +2591,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind // Images the shader only reads have nothing to store back. storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]); storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); + storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(storageTextures.size() - 1); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 695f83934..7e30db5e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1018,6 +1018,16 @@ VkImageView StorageTexture::AtomicView(std::uint32_t mip, bool firstLayer) { return created; } +VkImageView StorageTexture::Atomic64View(std::uint32_t mip, bool firstLayer) { + if (storageFormat == VK_FORMAT_R64_UINT) return firstLayer ? FirstLayerView(mip) : View(mip); + Require(storageFormat == VK_FORMAT_R32G32_UINT || storageFormat == VK_FORMAT_R32G32_SINT || storageFormat == VK_FORMAT_R32G32_SFLOAT, "64-bit storage image atomics need a surface of two 32-bit components"); + const auto found = atomicViews.find({mip, firstLayer}); + if (found != atomicViews.end()) return found->second; + const auto created = createView(mip, firstLayer, VK_FORMAT_R64_UINT); + atomicViews.emplace(std::pair{mip, firstLayer}, created); + return created; +} + const char* LookupOutcomes::Name(Kind kind) { static constexpr const char* names[Count] = {"sampled fast hit", "sampled fast miss", "sampled hit view", "sampled hit cleared view", "sampled hit snapshot", "sampled made view", "sampled made snapshot", "storage hit", "storage made", "refresh unchanged", "refresh compared", "upload direct", "upload cpu", "upload clear", "dcc scan", "pending flush"}; return kind < Count ? names[kind] : "?"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp new file mode 100644 index 000000000..79da0abe0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 64; +constexpr std::uint32_t Operations = 11; +constexpr std::uint32_t Format32_32UInt = 62; +constexpr std::uint32_t Format32_32SInt = 63; +constexpr std::uint32_t Format32_32Float = 64; +constexpr std::uint32_t Type2D = 9; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020085, 0x34060088, 0xe0381000, 0x80000401, 0xe0341010, 0x80001001, 0xbf8c3f70, 0x7e100300, + 0x7e120280, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf03c2308, + 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741000, 0x80010c03, + 0xe0741008, 0x80011203, 0x7e100300, 0x7e120281, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, + 0x7e180306, 0x7e1a0307, 0xf0442308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, + 0xbf8c3f70, 0xe0741010, 0x80010c03, 0xe0741018, 0x80011203, 0x7e100300, 0x7e120282, 0x7e140304, + 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0482308, 0x00020c08, 0x7e240280, + 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741020, 0x80010c03, 0xe0741028, 0x80011203, + 0x7e100300, 0x7e120283, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, + 0xf0502308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741030, + 0x80010c03, 0xe0741038, 0x80011203, 0x7e100300, 0x7e120284, 0x7e140304, 0x7e160305, 0xf03c0308, + 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0542308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, + 0x00021208, 0xbf8c3f70, 0xe0741040, 0x80010c03, 0xe0741048, 0x80011203, 0x7e100300, 0x7e120285, + 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0582308, 0x00020c08, + 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741050, 0x80010c03, 0xe0741058, + 0x80011203, 0x7e100300, 0x7e120286, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, + 0x7e1a0307, 0xf05c2308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, + 0xe0741060, 0x80010c03, 0xe0741068, 0x80011203, 0x7e100300, 0x7e120287, 0x7e140304, 0x7e160305, + 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0602308, 0x00020c08, 0x7e240280, 0x7e260280, + 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741070, 0x80010c03, 0xe0741078, 0x80011203, 0x7e100300, + 0x7e120288, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0642308, + 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741080, 0x80010c03, + 0xe0741088, 0x80011203, 0x7e100300, 0x7e120289, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, + 0x7e180306, 0x7e1a0307, 0xf0682308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, + 0xbf8c3f70, 0xe0741090, 0x80010c03, 0xe0741098, 0x80011203, 0x7e100300, 0x7e12028a, 0x7e140304, + 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0x7e1c0310, 0x7e1e0311, 0xf0402f08, + 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe07410a0, 0x80010c03, + 0xe07410a8, 0x80011203, 0xbf810000, +}; + +constexpr std::array, 12> Edges{{ + {0u, 0u}, {0u, 5u}, {5u, 0u}, {0x00000000ffffffffull, 1u}, + {0x0000000100000000ull, 1u}, {0x0000000100000000ull, 0x00000000ffffffffull}, {0x8000000000000000ull, 0x7fffffffffffffffull}, {0x7fffffffffffffffull, 0x8000000000000000ull}, + {0xffffffffffffffffull, 1u}, {1u, 0xffffffffffffffffull}, {0xffffffff00000000ull, 0x00000000ffffffffull}, {0x7fffffff00000000ull, 0x80000000ffffffffull}, +}}; + +std::uint64_t Word(const std::uint32_t* words) { + return words[0] | static_cast(words[1]) << 32u; +} + +void SetWord(std::uint32_t* words, std::uint64_t value) { + words[0] = static_cast(value); + words[1] = static_cast(value >> 32u); +} + +void FillInput() { + std::uint64_t state = 0x9e3779b97f4a7c15ull; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* words = &Input[tid * Inputs]; + for (std::uint32_t j = 0; j < 3u; ++j) { + state = state * 6364136223846793005ull + 1442695040888963407ull; + SetWord(words + 2u * j, state ^ (state << 29u)); + } + if (tid < Edges.size()) { + SetWord(words, Edges[tid][0]); + SetWord(words + 2, Edges[tid][1]); + } + if ((tid & 1u) != 0u) { + SetWord(words + 4, Word(words)); + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t width, std::uint32_t height, std::uint32_t format) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((height - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t format) { + Output.fill(0xdeadbeefu); + Texels.fill(0xdeadbeefu); + std::vector userData(16, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto texture = TextureDescriptor(Texels.data(), Threads, Operations, format); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +void Check(std::uint32_t format) { + constexpr std::array names{"image_atomic_swap", "image_atomic_add", "image_atomic_sub", "image_atomic_smin", "image_atomic_umin", "image_atomic_smax", "image_atomic_umax", "image_atomic_and", "image_atomic_or", "image_atomic_xor", "image_atomic_cmpswap"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto* in = &Input[tid * Inputs]; + const std::uint64_t a = Word(in); + const std::uint64_t b = Word(in + 2); + const std::uint64_t c = Word(in + 4); + const auto sa = static_cast(a); + const auto sb = static_cast(b); + const std::array expected{ + b, + a + b, + a - b, + static_cast(std::min(sa, sb)), + std::min(a, b), + static_cast(std::max(sa, sb)), + std::max(a, b), + a & b, + a | b, + a ^ b, + a == c ? b : a, + }; + for (std::uint32_t j = 0; j < Operations; ++j) { + const std::uint64_t returned = Word(&Output[tid * Results + 4u * j]); + const std::uint64_t stored = Word(&Output[tid * Results + 4u * j + 2u]); + Require(returned == a, std::string(names[j]) + ", format " + std::to_string(format) + ": thread " + std::to_string(tid) + " returned " + Hex(returned) + ", expected " + Hex(a)); + Require(stored == expected[j], std::string(names[j]) + ", format " + std::to_string(format) + ": thread " + std::to_string(tid) + " stored " + Hex(stored) + ", expected " + Hex(expected[j])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + for (const auto format : {Format32_32UInt, Format32_32SInt, Format32_32Float}) { + Run(*device, format); + Check(format); + } + std::puts("64-bit image atomics tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 7ea1c1823..bd2c34d3b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -11,7 +11,7 @@ namespace ShaderRecompiler { inline constexpr std::uint32_t FirstImageBinding = 1u; inline constexpr std::uint32_t FirstComparisonImageBinding = 22u; inline constexpr std::uint32_t FirstStorageImageBinding = 29u; -inline constexpr std::uint32_t ImageBindingCount = 43u; +inline constexpr std::uint32_t ImageBindingCount = 48u; enum class DescriptorBindingKind : std::uint32_t { Buffers = 0u, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index beb406829..a1cae49d2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -69,6 +69,7 @@ struct ImageResource { bool read = false; bool written = false; bool atomic = false; + bool atomic64 = false; bool depthCompare = false; bool cube = false; bool r128 = false; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index b0ced9a19..484c34519 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -444,6 +444,17 @@ enum class IrOpcode : std::uint16_t { ImageAtomicFCmpSwap32, ImageAtomicFMin32, ImageAtomicFMax32, + ImageAtomicSwap64, + ImageAtomicIAdd64, + ImageAtomicISub64, + ImageAtomicUMin64, + ImageAtomicUMax64, + ImageAtomicSMin64, + ImageAtomicSMax64, + ImageAtomicAnd64, + ImageAtomicOr64, + ImageAtomicXor64, + ImageAtomicCmpSwap64, GetAttribute, GetInterpolationParameter, SetAttribute, @@ -512,6 +523,7 @@ struct ExportFlags { [[nodiscard]] std::uint32_t SharedComponentCount(IrOpcode opcode); [[nodiscard]] AddressOpcodeInfo AddressOpcodeInfoOf(IrOpcode opcode); [[nodiscard]] ImageOpcodeInfo ImageOpcodeInfoOf(IrOpcode opcode); +bool IsImageAtomic64Opcode(IrOpcode opcode); [[nodiscard]] std::string_view IrOpcodeName(IrOpcode opcode); } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index bede0e6e5..5919c08e6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -485,6 +485,17 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("ImageAtomicFCmpSwap32", U32, ImageResource, ImageAddress, U32, U32, U1), makeMeta("ImageAtomicFMin32", U32, ImageResource, ImageAddress, U32, U1), makeMeta("ImageAtomicFMax32", U32, ImageResource, ImageAddress, U32, U1), + makeMeta("ImageAtomicSwap64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicIAdd64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicISub64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicUMin64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicUMax64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicSMin64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicSMax64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicAnd64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicOr64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicXor64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicCmpSwap64", U64, ImageResource, ImageAddress, U64, U64, U1), makeMeta("GetAttribute", U32, U32, U32), makeMeta("GetInterpolationParameter", U32, U32, U32, U32), makeMeta("SetAttribute", Void, U32x4, U1), @@ -804,12 +815,27 @@ ImageOpcodeInfo ImageOpcodeInfoOf(IrOpcode opcode) { case IrOpcode::ImageAtomicFCmpSwap32: case IrOpcode::ImageAtomicFMin32: case IrOpcode::ImageAtomicFMax32: + case IrOpcode::ImageAtomicSwap64: + case IrOpcode::ImageAtomicIAdd64: + case IrOpcode::ImageAtomicISub64: + case IrOpcode::ImageAtomicUMin64: + case IrOpcode::ImageAtomicUMax64: + case IrOpcode::ImageAtomicSMin64: + case IrOpcode::ImageAtomicSMax64: + case IrOpcode::ImageAtomicAnd64: + case IrOpcode::ImageAtomicOr64: + case IrOpcode::ImageAtomicXor64: + case IrOpcode::ImageAtomicCmpSwap64: return {ImageAccess::Atomic, ImageResourceClass::Storage, false}; default: return {}; } } +bool IsImageAtomic64Opcode(IrOpcode opcode) { + return opcode >= IrOpcode::ImageAtomicSwap64 && opcode <= IrOpcode::ImageAtomicCmpSwap64; +} + bool IsFloat64Opcode(IrOpcode opcode) { return opcode >= IrOpcode::FPAdd64 && opcode <= IrOpcode::ConvertU32F64; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 7a0d829a6..15aed9410 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -852,6 +852,7 @@ DescriptorBindingKind DescriptorBindingForImage(const ImageResource& image) { constexpr std::uint32_t storageFloatBinding = FirstStorageImageBinding; constexpr std::uint32_t storageUintBinding = storageFloatBinding + 5u; constexpr std::uint32_t atomicUintBinding = storageUintBinding + 5u; + constexpr std::uint32_t atomic64UintBinding = atomicUintBinding + 5u; std::uint32_t base = 0u; bool sampled = false; @@ -882,7 +883,7 @@ DescriptorBindingKind DescriptorBindingForImage(const ImageResource& image) { if (image.numericClass != IrTextureNumericClass::Uint) { fail("DescriptorBindingForImage atomic image must be uint"); } - base = atomicUintBinding; + base = image.atomic64 ? atomic64UintBinding : atomicUintBinding; } else { switch (image.numericClass) { case IrTextureNumericClass::Float: diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6791a87b8..e7182cfd8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -342,6 +342,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageWritten.push_back(image.written || image.atomic); physical.imageDepthCompare.push_back(image.depthCompare); physical.imageAtomic.push_back(image.atomic); + physical.imageAtomic64.push_back(image.atomic64); physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); physical.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource)); } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index fcb675049..2c46cb0a7 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -151,7 +151,10 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.dimension = descriptorDimension(descriptor, base.dimension); decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); - if (base.atomic && format != IrBufferFormat::Format32UInt && format != IrBufferFormat::Format32SInt && format != IrBufferFormat::Format32Float) { + if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { + throw std::runtime_error("64-bit atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); + } + if (base.atomic && !base.atomic64 && format != IrBufferFormat::Format32UInt && format != IrBufferFormat::Format32SInt && format != IrBufferFormat::Format32Float) { throw std::runtime_error("atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); } const bool storage = base.resourceClass == ImageResourceClass::Storage; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index bcd33a91c..d0b82d861 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -887,6 +887,11 @@ private: const auto access = ImageOpcodeInfoOf(op).access; const bool atomic = access == ImageAccess::Atomic; const bool write = access == ImageAccess::Write || atomic; + const bool atomic64 = IsImageAtomic64Opcode(op); + if ((image.read || image.written) && image.atomic64 != atomic64) { + throw std::runtime_error("an image accessed by 64-bit atomics is also accessed in another way"); + } + image.atomic64 = atomic64; image.firstUsePc = std::min(image.firstUsePc, pc); image.read = image.read || !write || atomic; image.written = image.written || write; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index b4d31ad94..4d0869dc2 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -348,7 +348,9 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 8u) & 15u; const bool compareSwap = info.opcode == RdnaOpcode::ImageAtomicCmpswap || info.opcode == RdnaOpcode::ImageAtomicFcmpswap; - if (dmask == 0u || (compareSwap ? dmask != 3u : (info.gather || info.atomic || msaaLoad) && !std::has_single_bit(dmask))) { + const bool floatAtomic = info.opcode == RdnaOpcode::ImageAtomicFcmpswap || info.opcode == RdnaOpcode::ImageAtomicFmin || info.opcode == RdnaOpcode::ImageAtomicFmax; + const bool atomic64 = info.atomic && !floatAtomic && !d16 && dmask == (compareSwap ? 15u : 3u); + if (dmask == 0u || (!atomic64 && (compareSwap ? dmask != 3u : (info.gather || info.atomic || msaaLoad) && !std::has_single_bit(dmask)))) { throw std::runtime_error("invalid MIMG data mask"); } if (d16 && !(info.sample || info.gather || opcode == 0u || opcode == 1u || opcode == 8u || opcode == 9u)) { @@ -397,7 +399,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span imageWritten; std::vector imageDepthCompare; std::vector imageAtomic; + std::vector imageAtomic64; // Guest buffer elements the shader updates atomically (one entry per element of a GuestBuffers // binding, empty otherwise). An atomic on a host-imported range is a serialized PCIe round trip // (~0.4-0.5 us each on NVIDIA), so a driver may keep these elements in device-local memory. diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 0f9f0d0dc..02aa4983b 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,7 +64,7 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(RecompileResult) == 176, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 408, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 448, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 28, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder"); @@ -249,6 +249,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageWritten); writer.Flags(binding.imageDepthCompare); writer.Flags(binding.imageAtomic); + writer.Flags(binding.imageAtomic64); writer.Flags(binding.bufferAtomic); writer.Flags(binding.bufferWritten); writer.Flags(binding.samplerUnnormalized); @@ -271,6 +272,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.imageWritten); reader.Flags(binding.imageDepthCompare); reader.Flags(binding.imageAtomic); + reader.Flags(binding.imageAtomic64); reader.Flags(binding.bufferAtomic); reader.Flags(binding.bufferWritten); reader.Flags(binding.samplerUnnormalized); @@ -407,6 +409,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.read); out.Value(image.written); out.Value(image.atomic); + out.Value(image.atomic64); out.Value(image.depthCompare); out.Value(image.cube); out.Value(image.r128); @@ -498,6 +501,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.read); in.Value(image.written); in.Value(image.atomic); + in.Value(image.atomic64); in.Value(image.depthCompare); in.Value(image.cube); in.Value(image.r128); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 0b433bfeb..f032957af 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -23,6 +23,7 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool imageInt64Atomics = false; bool sharedInt64Atomics = false; bool float64 = false; bool coherentBuffers = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 6080354b9..f558e5b85 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -361,6 +361,17 @@ void EmitImageAtomicDec32(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageAtomicFCmpSwap32(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageAtomicFMin32(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageAtomicFMax32(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicSwap64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicIAdd64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicISub64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicUMin64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicUMax64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicSMin64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicSMax64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicAnd64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicOr64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicXor64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicCmpSwap64(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitPhi(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitTessellationBase(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetThreadBitScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 927c8ca8c..9ea921ea8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -307,6 +307,9 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { if ((BufferAccessOf(inst->Opcode()) == BufferAccess::Atomic || addressAccess == AddressAccess::Atomic) && inst->Type() == IrType::U64) { requirements.bufferInt64Atomics = true; } + if (IsImageAtomic64Opcode(inst->Opcode())) { + requirements.imageInt64Atomics = true; + } if (IsFloat64Opcode(inst->Opcode())) { requirements.float64 = true; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 3f09507e4..496f4658d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -656,6 +656,17 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageAtomicFCmpSwap32: return Invoke(EmitImageAtomicFCmpSwap32, ctx, inst); case IrOpcode::ImageAtomicFMin32: return Invoke(EmitImageAtomicFMin32, ctx, inst); case IrOpcode::ImageAtomicFMax32: return Invoke(EmitImageAtomicFMax32, ctx, inst); + case IrOpcode::ImageAtomicSwap64: return Invoke(EmitImageAtomicSwap64, ctx, inst); + case IrOpcode::ImageAtomicIAdd64: return Invoke(EmitImageAtomicIAdd64, ctx, inst); + case IrOpcode::ImageAtomicISub64: return Invoke(EmitImageAtomicISub64, ctx, inst); + case IrOpcode::ImageAtomicUMin64: return Invoke(EmitImageAtomicUMin64, ctx, inst); + case IrOpcode::ImageAtomicUMax64: return Invoke(EmitImageAtomicUMax64, ctx, inst); + case IrOpcode::ImageAtomicSMin64: return Invoke(EmitImageAtomicSMin64, ctx, inst); + case IrOpcode::ImageAtomicSMax64: return Invoke(EmitImageAtomicSMax64, ctx, inst); + case IrOpcode::ImageAtomicAnd64: return Invoke(EmitImageAtomicAnd64, ctx, inst); + case IrOpcode::ImageAtomicOr64: return Invoke(EmitImageAtomicOr64, ctx, inst); + case IrOpcode::ImageAtomicXor64: return Invoke(EmitImageAtomicXor64, ctx, inst); + case IrOpcode::ImageAtomicCmpSwap64: return Invoke(EmitImageAtomicCmpSwap64, ctx, inst); case IrOpcode::GetAttribute: return Invoke(EmitGetAttribute, ctx, inst); case IrOpcode::GetInterpolationParameter: return Invoke(EmitGetInterpolationParameter, ctx, inst); case IrOpcode::SetAttribute: return Invoke(EmitSetAttribute, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 403ccb206..b0718b50e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -688,24 +688,34 @@ std::uint32_t PackedStoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess std::uint32_t ImageAtomicOpcode(IrOpcode opcode) { switch (opcode) { case IrOpcode::ImageAtomicSwap32: + case IrOpcode::ImageAtomicSwap64: return spv::OpAtomicExchange; case IrOpcode::ImageAtomicIAdd32: + case IrOpcode::ImageAtomicIAdd64: return spv::OpAtomicIAdd; case IrOpcode::ImageAtomicUMin32: + case IrOpcode::ImageAtomicUMin64: return spv::OpAtomicUMin; case IrOpcode::ImageAtomicUMax32: + case IrOpcode::ImageAtomicUMax64: return spv::OpAtomicUMax; case IrOpcode::ImageAtomicAnd32: + case IrOpcode::ImageAtomicAnd64: return spv::OpAtomicAnd; case IrOpcode::ImageAtomicOr32: + case IrOpcode::ImageAtomicOr64: return spv::OpAtomicOr; case IrOpcode::ImageAtomicXor32: + case IrOpcode::ImageAtomicXor64: return spv::OpAtomicXor; case IrOpcode::ImageAtomicISub32: + case IrOpcode::ImageAtomicISub64: return spv::OpAtomicISub; case IrOpcode::ImageAtomicSMin32: + case IrOpcode::ImageAtomicSMin64: return spv::OpAtomicSMin; case IrOpcode::ImageAtomicSMax32: + case IrOpcode::ImageAtomicSMax64: return spv::OpAtomicSMax; default: throw std::runtime_error("opcode is not an image atomic"); @@ -787,8 +797,27 @@ void EmitAtomicOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.Fail(access.inst, "atomics through a bindless image table are unsupported"); } const auto opcode = access.inst.Opcode(); + const auto condition = ctx.Arg(access.inst, access.inst.ArgumentCount() - 1u); + if (IsImageAtomic64Opcode(opcode)) { + ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU64(state), ConstantU64(state, 0u), [&]() { + const auto scalar = TypeScalarU64(state); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageTexelPointer, TypePointer(state, spv::StorageClassImage, scalar), pointer, StorageImageDescriptorPointer(state, access.mem.resource), CoordU32(ctx, access), ConstantU32(state, 0)); + const auto value = Unary(state, spv::OpBitcast, scalar, ctx.Arg(access.inst, 2)); + const auto old = state.module.AllocateId(); + if (opcode == IrOpcode::ImageAtomicCmpSwap64) { + const auto comparator = Unary(state, spv::OpBitcast, scalar, ctx.Arg(access.inst, 3)); + state.module.AddFunction(spv::OpAtomicCompareExchange, scalar, old, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, spv::MemorySemanticsMaskNone), value, comparator); + } else { + state.module.AddFunction(ImageAtomicOpcode(opcode), scalar, old, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), value); + } + EmitDeviceAtomicMemoryBarrier(state); + return Unary(state, spv::OpBitcast, TypeU64(state), old); + })); + return; + } const auto value = ctx.Arg(access.inst, 2); - ctx.Define(access.inst, EmitValueOrZeroIfCondition(state, ctx.Arg(access.inst, access.inst.ArgumentCount() - 1u), [&]() { + ctx.Define(access.inst, EmitValueOrZeroIfCondition(state, condition, [&]() { const auto pointer = state.module.AllocateId(); const auto pointerType = TypePointer(state, spv::StorageClassImage, TypeU32(state)); state.module.AddFunction(spv::OpImageTexelPointer, pointerType, pointer, StorageImageDescriptorPointer(state, access.mem.resource), CoordU32(ctx, access), ConstantU32(state, 0)); @@ -1213,6 +1242,17 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageAtomicFCmpSwap32: case IrOpcode::ImageAtomicFMin32: case IrOpcode::ImageAtomicFMax32: + case IrOpcode::ImageAtomicSwap64: + case IrOpcode::ImageAtomicIAdd64: + case IrOpcode::ImageAtomicISub64: + case IrOpcode::ImageAtomicUMin64: + case IrOpcode::ImageAtomicUMax64: + case IrOpcode::ImageAtomicSMin64: + case IrOpcode::ImageAtomicSMax64: + case IrOpcode::ImageAtomicAnd64: + case IrOpcode::ImageAtomicOr64: + case IrOpcode::ImageAtomicXor64: + case IrOpcode::ImageAtomicCmpSwap64: EmitAtomicOp(ctx, access); return; default: @@ -1320,6 +1360,50 @@ void EmitImageAtomicFMax32(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitImage(ctx, inst); } +void EmitImageAtomicSwap64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicIAdd64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicISub64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicUMin64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicUMax64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicSMin64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicSMax64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicAnd64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicOr64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicXor64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicCmpSwap64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto u32 = TypeU32(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 0fe12f8ef..99a8e78cc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -122,13 +122,14 @@ std::uint32_t ImageType(SpirvEmitterState& state, const ImageResource& image) { if (image.numericClass != IrTextureNumericClass::Uint) { FailEmit("atomic storage image must be uint"); } - format = spv::ImageFormatR32ui; + format = image.atomic64 ? spv::ImageFormatR64ui : spv::ImageFormatR32ui; } } else { FailEmit("invalid image resource class"); } const auto& info = RdnaImageDimensionInfoFor(image.dimension); - return state.module.Type(spv::OpTypeImage, ImageScalarType(state, image.numericClass), info.spirvDimension, image.depthCompare ? 1u : 0u, info.arrayed, info.multisampled, sampled, format); + const auto scalar = image.atomic64 ? TypeScalarU64(state) : ImageScalarType(state, image.numericClass); + return state.module.Type(spv::OpTypeImage, scalar, info.spirvDimension, image.depthCompare ? 1u : 0u, info.arrayed, info.multisampled, sampled, format); } std::uint32_t ImageViewSizeType(SpirvEmitterState& state, RdnaImageDimension dimension) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9b42dde31..27eea3e66 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -122,6 +122,15 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.sharedInt64Atomics) { state.module.EmitCapability(spv::CapabilityInt64); } + if (state.requirements.imageInt64Atomics) { + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64ImageEXT)) == state.supportedCapabilities.end()) { + FailEmit("64-bit image atomics need VK_EXT_shader_image_atomic_int64"); + } + state.module.EmitCapability(spv::CapabilityInt64); + state.module.EmitCapability(spv::CapabilityInt64Atomics); + state.module.EmitCapability(spv::CapabilityInt64ImageEXT); + state.module.EmitExtension("SPV_EXT_shader_image_int64"); + } if (state.clipDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityClipDistance); } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 0207f3e53..6be88ccce 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -41,6 +41,23 @@ MemoryInfo imageMemoryInfoFromInstruction(const RdnaInstruction& inst) { return memory; } +IrOpcode imageAtomic64Opcode(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::ImageAtomicSwap32: return IrOpcode::ImageAtomicSwap64; + case IrOpcode::ImageAtomicIAdd32: return IrOpcode::ImageAtomicIAdd64; + case IrOpcode::ImageAtomicISub32: return IrOpcode::ImageAtomicISub64; + case IrOpcode::ImageAtomicUMin32: return IrOpcode::ImageAtomicUMin64; + case IrOpcode::ImageAtomicUMax32: return IrOpcode::ImageAtomicUMax64; + case IrOpcode::ImageAtomicSMin32: return IrOpcode::ImageAtomicSMin64; + case IrOpcode::ImageAtomicSMax32: return IrOpcode::ImageAtomicSMax64; + case IrOpcode::ImageAtomicAnd32: return IrOpcode::ImageAtomicAnd64; + case IrOpcode::ImageAtomicOr32: return IrOpcode::ImageAtomicOr64; + case IrOpcode::ImageAtomicXor32: return IrOpcode::ImageAtomicXor64; + case IrOpcode::ImageAtomicCmpSwap32: return IrOpcode::ImageAtomicCmpSwap64; + default: throw std::runtime_error("64-bit " + std::string(IrOpcodeName(opcode)) + " is not implemented"); + } +} + } bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { @@ -72,17 +89,28 @@ bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { } bool TranslationContext::imageAtomic(const RdnaInstruction& inst, IrOpcode opcode) { + if (inst.dataBits == 64u) { + opcode = imageAtomic64Opcode(opcode); + } const MemoryInfo memory = imageMemoryInfoFromInstruction(inst); IrValue* resource = getImageResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); - const IrU32 value = readU32(inst.destination); IrValue& exec = ir.GetExec(); IrValue* result; - if (opcode == IrOpcode::ImageAtomicCmpSwap32 || opcode == IrOpcode::ImageAtomicFCmpSwap32) { + if (opcode == IrOpcode::ImageAtomicCmpSwap64) { + const IrU64 value = readU64(inst.destination); + const IrU64 comparator = readU64(offsetOperand(inst.destination, 2u)); + result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &comparator.Value(), &exec}, flags); + } else if (IsImageAtomic64Opcode(opcode)) { + const IrU64 value = readU64(inst.destination); + result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &exec}, flags); + } else if (opcode == IrOpcode::ImageAtomicCmpSwap32 || opcode == IrOpcode::ImageAtomicFCmpSwap32) { + const IrU32 value = readU32(inst.destination); const IrU32 comparator = readU32(offsetOperand(inst.destination, 1u)); result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &comparator.Value(), &exec}, flags); } else { + const IrU32 value = readU32(inst.destination); result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &exec}, flags); } if (inst.glc) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c4ee68aa9..cf0665c53 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -191,6 +191,7 @@ Throughout the project, every function at every stage either **does exactly what - An [unnormalized S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. - [`ds_wrxchg2_rtn_b32` and `ds_wrxchg2st64_rtn_b32`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. +- [64-bit image atomics](../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). From 1e28efbd26d3e3dabd57c354953c119fc257e3dd Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 7 Oct 2026 10:25:07 +0200 Subject: [PATCH 175/203] feat(agc): accept 64-bit image atomics in graphics shaders A pixel or vertex shader with a 64-bit image atomic declares Int64Atomics, Int64ImageEXT and SPV_EXT_shader_image_int64, which graphics shader validation rejected ("SPIR-V requires unsupported device capability"), so the draw was dropped. Compute dispatches are not validated this way. The device records whether it enabled shaderImageInt64Atomics (imageInt64Atomics in the device state and the graphics Context), and ValidateShaders accepts the two capabilities and the extension when it did. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 3 +++ .../prx/libSceAgcDriver/Graphics/include/Context.hpp | 1 + .../prx/libSceAgcDriver/Graphics/include/Pipeline.hpp | 2 +- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../libSceAgcDriver/Graphics/src/ShaderValidation.cpp | 11 ++++++----- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c17cfcdf5..c4bf9972a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -191,6 +191,7 @@ struct VulkanDevice::State { // 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; + bool imageInt64Atomics = false; bool primitiveListRestart = false; bool depthClipControl = false; bool imageViewMinLod = false; @@ -759,6 +760,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u if (imageInt64Atomics) { deviceExtensions.push_back(VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityInt64ImageEXT); + state->imageInt64Atomics = true; state->spirvExtensions.push_back("SPV_EXT_shader_image_int64"); } deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); @@ -2419,6 +2421,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.copiedWriters = state->copiedWriters.get(); context.functions = state->functionsReady ? &state->deviceFunctions : nullptr; context.descriptorIndexing = state->descriptorIndexing; + context.imageInt64Atomics = state->imageInt64Atomics; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index befeff74c..eebe5ff7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -134,6 +134,7 @@ struct Context { // VK_EXT_descriptor_indexing with non-uniform sampled/storage image array indexing enabled // (bindless image tables in graphics stages). bool descriptorIndexing = false; + bool imageInt64Atomics = false; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index fe55c0dce..27e6293ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -89,7 +89,7 @@ void ValidateDepthBounds(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index aafdb1e31..cd8095a8f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -518,7 +518,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 67a61cc4e..296b7f0ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -123,7 +123,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -235,7 +235,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isTessellationCapability || isMeshCapability || isFeatureCapability || - isDescriptorIndexingCapability, + isDescriptorIndexingCapability || + (imageInt64Atomics && (capability == spv::CapabilityInt64Atomics || capability == spv::CapabilityInt64ImageEXT)), std::string("SPIR-V requires unsupported device capability ") + std::to_string(static_cast(capability))); @@ -252,7 +253,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } - Require(extension == "SPV_KHR_float_controls" || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } case spv::OpDecorateId: @@ -491,7 +492,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -507,7 +508,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9be7d4dce..66757ee7d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1804,6 +1804,15 @@ void validationTests() { attribute.resource.fields[1] |= 0x80000000u; expectFailure([&] { AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)); }, "descriptor flags"); } + for (const auto capability : {spv::CapabilityInt64Atomics, spv::CapabilityInt64ImageEXT}) { + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast(capability)}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, true); + } for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); From 55447e1c7770eaea700da7829466bb2d2fd4e1c9 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Mon, 5 Oct 2026 20:20:51 +0200 Subject: [PATCH 176/203] fix(recompiler): read a coherent dwordx2 as one 64-bit atomic load A glc buffer_load_dwordx2 is one 64-bit access on the GPU, so it never sees half of a buffer_atomic_swap_x2. It was emitted as two dword loads, which can: the first can read the flag from before the swap and the second the value from after it. GTA V gives its BVH nodes their record indices with a single-pass decoupled look-back prefix sum. Each workgroup publishes a {flag, value} pair for its block with buffer_atomic_swap_x2 and reads the previous 64 blocks' pairs with glc dwordx2 loads. A torn read counts earlier blocks twice, two nodes then share records, the BVH is corrupt and the ray tracing refit hangs. It only happens in scans of more than 64 blocks (the largest BVHs) and depends on timing. In a program that has the u64 storage-buffer view (it uses 64-bit buffer atomics), a coherent raw two-dword buffer load that is 8-byte aligned and in bounds is now read with one device-scope OpAtomicLoad on that view and split with OpUConvert and a shift (not OpBitcast to a uvec2, which crashes NVIDIA's shader compiler). All other loads keep the per-dword path. tests/execution/LookBackScan runs the game's scan kernel: the look-back from given blocks, and 50 whole 125-block scans checked against a CPU prefix sum. Without this change the first whole scan fails. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/execution/LookBackScan.cpp | 190 ++++++++++++++++++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 24 +++ 2 files changed, 214 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp new file mode 100644 index 000000000..3e098baae --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -0,0 +1,190 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::array ScanCode{ + 0xbfa00003u, 0x878e817eu, 0x8811ff05u, 0x00080000u, 0xbefe040eu, 0xbe900304u, 0xbe92030bu, 0xbe9303ffu, + 0x00016204u, 0x8805ff07u, 0x00040000u, 0xbe840306u, 0xbe86030cu, 0xbe8703ffu, 0x00016204u, 0xbf880003u, + 0x7e020281u, 0xe0c84000u, 0x80000100u, 0xbf8c3f70u, 0xd7600002u, 0x00010101u, 0xbefe04c1u, 0x9003860au, + 0xbf0a0380u, 0x93000a02u, 0x4a240000u, 0x7d88240cu, 0xbf840006u, 0x7e020280u, 0xbefe046au, 0xbf880002u, + 0xe0302000u, 0x80010112u, 0xbefe04c1u, 0xd76d0011u, 0x04018000u, 0xbf0a0381u, 0x7d88220cu, 0xbf840006u, + 0x7e040280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x80010211u, 0xbefe04c1u, 0xd76d0010u, 0x0401fe00u, + 0x00000080u, 0xbf0a0382u, 0x7d88200cu, 0xbf840006u, 0x7e060280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, + 0x80010310u, 0xbefe04c1u, 0xd76d000fu, 0x0401fe00u, 0x000000c0u, 0xbf0a0383u, 0x7d881e0cu, 0xbf840006u, + 0x7e080280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x8001040fu, 0xbefe04c1u, 0xd76d000eu, 0x0401fe00u, + 0x00000100u, 0xbf0a0384u, 0x7d881c0cu, 0xbf840006u, 0x7e0a0280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, + 0x8001050eu, 0xbefe04c1u, 0xd76d000du, 0x0401fe00u, 0x00000140u, 0xbf0a0385u, 0x7d881a0cu, 0xbf840006u, + 0x7e0c0280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x8001060du, 0xbefe04c1u, 0xd76d000cu, 0x0401fe00u, + 0x00000180u, 0xbf0a0386u, 0x7d88180cu, 0xbf840006u, 0x7e0e0280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, + 0x8001070cu, 0xbefe04c1u, 0xd76d000bu, 0x0401fe00u, 0x000001c0u, 0xbf0a0387u, 0x7d88160cu, 0xbf840006u, + 0x7e100280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x8001080bu, 0xbefe04c1u, 0xbe8d0380u, 0xbe8a0380u, + 0xbf0a030du, 0xbf840020u, 0xbefc030du, 0x810d810du, 0xbf8c3f70u, 0x7e148701u, 0x7e12030au, 0xbeea287eu, + 0x02281480u, 0x4a2828fau, 0xff011114u, 0x4a2828fau, 0xff011214u, 0x4a2828fau, 0xff011414u, 0x4a2828fau, + 0xff011814u, 0xd7781013u, 0x03058314u, 0x4a2826fau, 0xaf00e414u, 0x92fea0a0u, 0xd7600000u, 0x00013f14u, + 0x4a282800u, 0xbefe046au, 0x4c121314u, 0x4a14130au, 0x4a12120au, 0xd760006au, 0x00017f0au, 0x7e028509u, + 0x810a6a0au, 0xbf82ffdeu, 0xbefe0481u, 0x7e260202u, 0xbf088002u, 0x7e14020au, 0x856a8281u, 0x7e12026au, + 0xe1402000u, 0x80040913u, 0xbf088002u, 0xbe8d0380u, 0xbefe04c1u, 0xbf840058u, 0xd76d0013u, 0x0401fe02u, + 0xffffffc0u, 0xbe8d0380u, 0x7d0626f9u, 0x06868080u, 0x7e120281u, 0x7e140280u, 0xbefe0400u, 0xbf880002u, + 0xe034e000u, 0x80040913u, 0xbefe04c1u, 0xbf8c3f70u, 0x7d041280u, 0xbf13806au, 0xbf840006u, 0xbefe0400u, + 0xbf880002u, 0xe034e000u, 0x80040913u, 0xbefe04c1u, 0xbf82fff6u, 0x7d0412f9u, 0x06868082u, 0xbf138000u, + 0xbf850016u, 0x4e2626c0u, 0xbeea287eu, 0x02121480u, 0x4a1212fau, 0xff011109u, 0x4a1212fau, 0xff011209u, + 0x4a1212fau, 0xff011409u, 0x4a1212fau, 0xff011809u, 0xd778100au, 0x03058309u, 0x4a121509u, 0xbefe046au, + 0xd7600000u, 0x00013f09u, 0xd7600001u, 0x00017f09u, 0x81140100u, 0x810d140du, 0xbf82ffd3u, 0x7e127201u, + 0x7d8a12c1u, 0x4c1212bfu, 0xbf870002u, 0x7e127200u, 0x4c12129fu, 0x7d881300u, 0x7e000509u, 0x02001480u, + 0xbeea287eu, 0xd7600014u, 0x0000010au, 0x02120080u, 0x4a1212fau, 0xff011109u, 0x4a1212fau, 0xff011209u, + 0x4a1212fau, 0xff011409u, 0x4a1212fau, 0xff011809u, 0xd7781000u, 0x03058309u, 0x4a120109u, 0xbefe0481u, + 0xd7600000u, 0x00013f09u, 0xd7600001u, 0x00017f09u, 0x81150100u, 0x816a1514u, 0x810d0d6au, 0x7e000202u, + 0x4a141af9u, 0x8686060au, 0x7e120282u, 0xe1402000u, 0x80040900u, 0xbefe04c1u, 0x7d86240cu, 0xbf090380u, + 0xbf800000u, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00020du, 0xe0702000u, + 0x80010012u, 0xbefe04c1u, 0xbf090381u, 0x7d86220cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, + 0xbf8c3f70u, 0x4a00040du, 0xe0702000u, 0x80010011u, 0xbefe04c1u, 0xbf090382u, 0x7d86200cu, 0x8580807eu, + 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00060du, 0xe0702000u, 0x80010010u, 0xbefe04c1u, + 0xbf090383u, 0x7d861e0cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00080du, + 0xe0702000u, 0x8001000fu, 0xbefe04c1u, 0xbf090384u, 0x7d861c0cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, + 0xbf880004u, 0xbf8c3f70u, 0x4a000a0du, 0xe0702000u, 0x8001000eu, 0xbefe04c1u, 0xbf090385u, 0x7d861a0cu, + 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a000c0du, 0xe0702000u, 0x8001000du, + 0xbefe04c1u, 0xbf090386u, 0x7d86180cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, + 0x4a000e0du, 0xe0702000u, 0x8001000cu, 0xbefe04c1u, 0xbf090387u, 0x7d86160cu, 0x8580807eu, 0x8dea006au, + 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00100du, 0xe0702000u, 0x8001000bu, 0x816ac10bu, 0xbefe04c1u, + 0xbf066a02u, 0xbf800000u, 0x85ea807eu, 0x87ea0e6au, 0xbefe046au, 0xbf880008u, 0x4a0014f9u, 0x8686060du, + 0xbe8a0381u, 0xbe8b03ffu, 0x00016204u, 0xbe891d92u, 0xe0700000u, 0x80020000u, 0xbf810000u, 0xbf9f0000u, + 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, + 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, + 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, + 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0x00000000u, 0x00000000u, + 0x30306c73u, 0x0000013fu, 0x00000156u, 0x00000000u, 0x00102557u, 0x0401a600u, 0x0b100a10u, 0x40100c10u, + 0x4c104b40u, 0x4e104d10u, 0x79606f10u, 0x00107a10u, 0x00006e00u, 0x21200f64u, 0x25402410u, 0x64000010u, + 0x40241021u, 0x28600000u, 0x00102950u, 0x30c9a100u, 0x9e0800c0u, 0xa0841941u, 0x18c08418u, 0x8418c084u, + 0xc08418c0u, 0x18c08418u, 0x8418c084u, 0xe0c01040u, 0x08430820u, 0x08104108u, 0x2334108bu, 0x38838838u, + 0x88388388u, 0x83883883u, 0x80f01040u, 0x98241e05u, 0x0d8442e0u, 0xe1183e0fu, 0x17158644u, 0x46e1985eu, + 0x7e1f1d88u, 0x8a48e218u, 0x989e2725u, 0x2d8c4ae2u, 0xe318be2fu, 0x37358e4cu, 0x4ee398deu, 0xfe3f3d90u, + 0x9250e418u, 0x991e4745u, 0x4f4d92e4u, 0x6a25112eu, 0xe6ab29cau, 0xcb6b32bcu, 0x2afcbb2du, 0x31cc6c33u, + 0xf34b3cbbu, 0xdb36b5cau, 0x6716f378u, 0xddd6739cu, 0x9ede6779u, 0xf9fdf67bu, 0x83a0e067u, 0x687a1e16u, + 0x368ba2e2u, 0xe468fa3eu, 0x5e5693a4u, 0x68a6697au, 0xf7777777u, 0x5554fdddu, 0xcb000205u, 0x70817d02u, + 0x00080019u, 0x003f0146u, 0xa41002a4u, 0x3402a602u, 0xaa000a34u, 0xaa100c02u, 0x3402b002u, 0x1502b631u, + 0x400202bcu, 0x02b60101u, 0x8b2ef007u, 0xd74d0005u, 0xe0828001u, 0x00001601u, 0x00000000u, 0x00000000u, + 0x65726162u, 0x746f6f66u, 0x442e4a87u, 0x00000000u, 0x00000001u, 0x00000618u, 0x00000000u, 0x00000156u, + 0x980d163fu, 0xeddd574eu, 0x00000000u, 0x00000000u, +}; + +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t PerBlock = 256; +constexpr std::uint32_t Blocks = 125; +constexpr std::uint32_t Elements = 31860; + +alignas(256) std::array Counter{}; +alignas(256) std::array Pairs{}; +alignas(256) std::array Data{}; +alignas(256) std::array Total{}; + +std::uint32_t Value(std::uint32_t index, std::uint32_t seed) { + return ((index * 2654435761u + seed * 40503u) >> 13u) % 3u; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { + alignas(256) static std::array code{}; + code = ScanCode; + const auto address = [](const void* data) { return reinterpret_cast(data); }; + std::vector userData{ + static_cast(address(Counter.data())), static_cast((address(Counter.data()) >> 32u) & 0xffffu) | 0x00040000u, 1u, 0x00005204u, + static_cast(address(Pairs.data())), static_cast(address(Pairs.data()) >> 32u), + static_cast(address(Data.data())), static_cast(address(Data.data()) >> 32u), + static_cast(address(Total.data())), static_cast(address(Total.data()) >> 32u), + PerBlock, Blocks, Elements}; + const std::span words(code); + const std::array memory{{{address(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, address(code.data()), words, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, groups, 1, 1, {}, address(code.data())); + device.WaitIdle(); +} + +std::uint32_t BlockSum(const std::vector& input, std::uint32_t block) { + std::uint32_t sum = 0; + for (std::uint32_t i = block * PerBlock; i < (block + 1) * PerBlock && i < Elements; ++i) sum += input[i]; + return sum; +} + +void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { + const auto name = "look-back from block " + std::to_string(block); + std::vector input(Data.size(), 0u); + for (std::uint32_t i = 0; i < Elements; ++i) input[i] = Value(i, block); + Pairs.fill(0u); + std::uint32_t prefix = 0; + for (std::uint32_t b = 0; b < block; ++b) { + const auto sum = BlockSum(input, b); + prefix += sum; + Pairs[2 * b] = b == 0 ? 2u : 1u; + Pairs[2 * b + 1] = b == 0 ? prefix : sum; + } + Counter.fill(0u); + Counter[0] = block; + std::copy(input.begin(), input.end(), Data.begin()); + Total.fill(0xdeadbeefu); + Run(device, 1); + Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0])); + Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2"); + Require(Pairs[2 * block + 1] == prefix + BlockSum(input, block), name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + BlockSum(input, block)) + " (prefix " + std::to_string(prefix) + ")"); + std::uint32_t running = prefix; + for (std::uint32_t i = block * PerBlock; i < (block + 1) * PerBlock && i < Elements; ++i) { + Require(Data[i] == running, name + ": element " + std::to_string(i) + " is " + std::to_string(Data[i]) + ", expected " + std::to_string(running) + " (prefix " + std::to_string(prefix) + ")"); + running += input[i]; + } + if (block == Blocks - 1) Require(Total[0] == running, name + ": the total is " + std::to_string(Total[0]) + ", expected " + std::to_string(running)); +} + +void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) { + const auto name = "whole scan " + std::to_string(seed); + std::vector input(Data.size(), 0u); + for (std::uint32_t i = 0; i < Elements; ++i) input[i] = Value(i, seed); + Counter.fill(0u); + Pairs.fill(0u); + std::copy(input.begin(), input.end(), Data.begin()); + Total.fill(0xdeadbeefu); + Run(device, Blocks); + std::uint32_t running = 0; + for (std::uint32_t i = 0; i < Elements; ++i) { + Require(Data[i] == running, name + ": element " + std::to_string(i) + " (block " + std::to_string(i / PerBlock) + ") is " + std::to_string(Data[i]) + ", expected " + std::to_string(running)); + running += input[i]; + } + Require(Total[0] == running, name + ": the total is " + std::to_string(Total[0]) + ", expected " + std::to_string(running)); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const std::uint32_t block : {0u, 1u, 50u, 63u, 64u, 65u, 100u, 124u}) CheckLookBack(*device, block); + for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed); + std::puts("look-back scan tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 550803e92..dee96aab9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -580,11 +580,35 @@ void FormattedStore(SpirvValueEmitContext& ctx, const IrValue& inst, const Memor }); } +std::uint32_t LoadCoherentPair(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource) { + auto& state = ctx.state; + const auto wide = PrepareStorageBufferResourceAccess(state, mem, state.storageBufferU64Variable, TypeStorageBufferU64Pointer(state)); + const auto byteAddress = Binary(state, spv::OpIAdd, TypeU32(state), ByteAddress(ctx, inst, mem), wide.byteOffset); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byteAddress, ConstantU32(state, 3u)); + const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteAddress, ConstantU32(state, 7u)), ConstantU32(state, 0u)); + return EmitValueIfElse(state, AndCondition(state, aligned, EmitMemoryElementInBounds(state, wide, index)), TypeU32Composite(state, 2u), [&]() { + const auto pointer = EmitStorageBufferElementPointer(state, wide, index, TypeStorageBufferU64ElementPointer(state)); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicLoad, TypeScalarU64(state), value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone)); + const auto high = Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), value, BdaConstant(state, 32u)); + return ConstructU32Composite(state, 2u, {Unary(state, spv::OpUConvert, TypeU32(state), value), Unary(state, spv::OpUConvert, TypeU32(state), high), 0u, 0u}); + }, [&]() { + std::array values{}; + for (std::uint32_t component = 0; component < 2u; component++) { + values.at(component) = LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, component), resource); + } + return ConstructU32Composite(state, 2u, values); + }); +} + std::uint32_t LoadWideBuffer(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 info = MemoryFormatInfo(state, mem); + if (info.type == SpirvFormatComponentType::Unknown && components == 2u && mem.coherent && state.storageBufferU64Variable != 0u) { + return LoadCoherentPair(ctx, inst, mem, resource); + } if (info.type != SpirvFormatComponentType::Unknown) { const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, components, FormattedAccess::Load); return EmitValueOrDefaultIfCondition(state, plan.inBounds, TypeU32Composite(state, components), FormattedOutOfBoundsValue(ctx, mem, plan, components), [&]() { From 58eb55dddf5186be24697263bcca2e7b6e18241d Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Wed, 7 Oct 2026 10:57:23 +0200 Subject: [PATCH 177/203] test(recompiler): a hand-written look-back kernel instead of the game's; record the dwordx2 loads that can still tear LookBackScan runs a 43-instruction decoupled look-back written for the test: one aggregate per block, the block index from an atomic counter, {flag, value} published with buffer_atomic_swap_x2 and read back with glc buffer_load_dwordx2. Seven single look-backs and 50 scans of 4096 blocks are checked against a CPU prefix sum; without the recompiler change the first whole scan fails (3 of 3 runs). TechnicalDebt records that only glc, aligned, in-bounds dwordx2 loads in programs with the 64-bit storage view are one atomic load: loads without glc, and loads in programs without 64-bit buffer atomics, can still tear against a concurrent swap_x2. --- .../tests/execution/LookBackScan.cpp | 184 ++++++++---------- docs/dev/TechnicalDebt.md | 1 + 2 files changed, 77 insertions(+), 108 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp index 3e098baae..e36b005c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -14,159 +14,127 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::array ScanCode{ - 0xbfa00003u, 0x878e817eu, 0x8811ff05u, 0x00080000u, 0xbefe040eu, 0xbe900304u, 0xbe92030bu, 0xbe9303ffu, - 0x00016204u, 0x8805ff07u, 0x00040000u, 0xbe840306u, 0xbe86030cu, 0xbe8703ffu, 0x00016204u, 0xbf880003u, - 0x7e020281u, 0xe0c84000u, 0x80000100u, 0xbf8c3f70u, 0xd7600002u, 0x00010101u, 0xbefe04c1u, 0x9003860au, - 0xbf0a0380u, 0x93000a02u, 0x4a240000u, 0x7d88240cu, 0xbf840006u, 0x7e020280u, 0xbefe046au, 0xbf880002u, - 0xe0302000u, 0x80010112u, 0xbefe04c1u, 0xd76d0011u, 0x04018000u, 0xbf0a0381u, 0x7d88220cu, 0xbf840006u, - 0x7e040280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x80010211u, 0xbefe04c1u, 0xd76d0010u, 0x0401fe00u, - 0x00000080u, 0xbf0a0382u, 0x7d88200cu, 0xbf840006u, 0x7e060280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, - 0x80010310u, 0xbefe04c1u, 0xd76d000fu, 0x0401fe00u, 0x000000c0u, 0xbf0a0383u, 0x7d881e0cu, 0xbf840006u, - 0x7e080280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x8001040fu, 0xbefe04c1u, 0xd76d000eu, 0x0401fe00u, - 0x00000100u, 0xbf0a0384u, 0x7d881c0cu, 0xbf840006u, 0x7e0a0280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, - 0x8001050eu, 0xbefe04c1u, 0xd76d000du, 0x0401fe00u, 0x00000140u, 0xbf0a0385u, 0x7d881a0cu, 0xbf840006u, - 0x7e0c0280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x8001060du, 0xbefe04c1u, 0xd76d000cu, 0x0401fe00u, - 0x00000180u, 0xbf0a0386u, 0x7d88180cu, 0xbf840006u, 0x7e0e0280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, - 0x8001070cu, 0xbefe04c1u, 0xd76d000bu, 0x0401fe00u, 0x000001c0u, 0xbf0a0387u, 0x7d88160cu, 0xbf840006u, - 0x7e100280u, 0xbefe046au, 0xbf880002u, 0xe0302000u, 0x8001080bu, 0xbefe04c1u, 0xbe8d0380u, 0xbe8a0380u, - 0xbf0a030du, 0xbf840020u, 0xbefc030du, 0x810d810du, 0xbf8c3f70u, 0x7e148701u, 0x7e12030au, 0xbeea287eu, - 0x02281480u, 0x4a2828fau, 0xff011114u, 0x4a2828fau, 0xff011214u, 0x4a2828fau, 0xff011414u, 0x4a2828fau, - 0xff011814u, 0xd7781013u, 0x03058314u, 0x4a2826fau, 0xaf00e414u, 0x92fea0a0u, 0xd7600000u, 0x00013f14u, - 0x4a282800u, 0xbefe046au, 0x4c121314u, 0x4a14130au, 0x4a12120au, 0xd760006au, 0x00017f0au, 0x7e028509u, - 0x810a6a0au, 0xbf82ffdeu, 0xbefe0481u, 0x7e260202u, 0xbf088002u, 0x7e14020au, 0x856a8281u, 0x7e12026au, - 0xe1402000u, 0x80040913u, 0xbf088002u, 0xbe8d0380u, 0xbefe04c1u, 0xbf840058u, 0xd76d0013u, 0x0401fe02u, - 0xffffffc0u, 0xbe8d0380u, 0x7d0626f9u, 0x06868080u, 0x7e120281u, 0x7e140280u, 0xbefe0400u, 0xbf880002u, - 0xe034e000u, 0x80040913u, 0xbefe04c1u, 0xbf8c3f70u, 0x7d041280u, 0xbf13806au, 0xbf840006u, 0xbefe0400u, - 0xbf880002u, 0xe034e000u, 0x80040913u, 0xbefe04c1u, 0xbf82fff6u, 0x7d0412f9u, 0x06868082u, 0xbf138000u, - 0xbf850016u, 0x4e2626c0u, 0xbeea287eu, 0x02121480u, 0x4a1212fau, 0xff011109u, 0x4a1212fau, 0xff011209u, - 0x4a1212fau, 0xff011409u, 0x4a1212fau, 0xff011809u, 0xd778100au, 0x03058309u, 0x4a121509u, 0xbefe046au, - 0xd7600000u, 0x00013f09u, 0xd7600001u, 0x00017f09u, 0x81140100u, 0x810d140du, 0xbf82ffd3u, 0x7e127201u, - 0x7d8a12c1u, 0x4c1212bfu, 0xbf870002u, 0x7e127200u, 0x4c12129fu, 0x7d881300u, 0x7e000509u, 0x02001480u, - 0xbeea287eu, 0xd7600014u, 0x0000010au, 0x02120080u, 0x4a1212fau, 0xff011109u, 0x4a1212fau, 0xff011209u, - 0x4a1212fau, 0xff011409u, 0x4a1212fau, 0xff011809u, 0xd7781000u, 0x03058309u, 0x4a120109u, 0xbefe0481u, - 0xd7600000u, 0x00013f09u, 0xd7600001u, 0x00017f09u, 0x81150100u, 0x816a1514u, 0x810d0d6au, 0x7e000202u, - 0x4a141af9u, 0x8686060au, 0x7e120282u, 0xe1402000u, 0x80040900u, 0xbefe04c1u, 0x7d86240cu, 0xbf090380u, - 0xbf800000u, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00020du, 0xe0702000u, - 0x80010012u, 0xbefe04c1u, 0xbf090381u, 0x7d86220cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, - 0xbf8c3f70u, 0x4a00040du, 0xe0702000u, 0x80010011u, 0xbefe04c1u, 0xbf090382u, 0x7d86200cu, 0x8580807eu, - 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00060du, 0xe0702000u, 0x80010010u, 0xbefe04c1u, - 0xbf090383u, 0x7d861e0cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00080du, - 0xe0702000u, 0x8001000fu, 0xbefe04c1u, 0xbf090384u, 0x7d861c0cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, - 0xbf880004u, 0xbf8c3f70u, 0x4a000a0du, 0xe0702000u, 0x8001000eu, 0xbefe04c1u, 0xbf090385u, 0x7d861a0cu, - 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a000c0du, 0xe0702000u, 0x8001000du, - 0xbefe04c1u, 0xbf090386u, 0x7d86180cu, 0x8580807eu, 0x8dea006au, 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, - 0x4a000e0du, 0xe0702000u, 0x8001000cu, 0xbefe04c1u, 0xbf090387u, 0x7d86160cu, 0x8580807eu, 0x8dea006au, - 0xbefe046au, 0xbf880004u, 0xbf8c3f70u, 0x4a00100du, 0xe0702000u, 0x8001000bu, 0x816ac10bu, 0xbefe04c1u, - 0xbf066a02u, 0xbf800000u, 0x85ea807eu, 0x87ea0e6au, 0xbefe046au, 0xbf880008u, 0x4a0014f9u, 0x8686060du, - 0xbe8a0381u, 0xbe8b03ffu, 0x00016204u, 0xbe891d92u, 0xe0700000u, 0x80020000u, 0xbf810000u, 0xbf9f0000u, - 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, - 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, - 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, - 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0xbf9f0000u, 0x00000000u, 0x00000000u, - 0x30306c73u, 0x0000013fu, 0x00000156u, 0x00000000u, 0x00102557u, 0x0401a600u, 0x0b100a10u, 0x40100c10u, - 0x4c104b40u, 0x4e104d10u, 0x79606f10u, 0x00107a10u, 0x00006e00u, 0x21200f64u, 0x25402410u, 0x64000010u, - 0x40241021u, 0x28600000u, 0x00102950u, 0x30c9a100u, 0x9e0800c0u, 0xa0841941u, 0x18c08418u, 0x8418c084u, - 0xc08418c0u, 0x18c08418u, 0x8418c084u, 0xe0c01040u, 0x08430820u, 0x08104108u, 0x2334108bu, 0x38838838u, - 0x88388388u, 0x83883883u, 0x80f01040u, 0x98241e05u, 0x0d8442e0u, 0xe1183e0fu, 0x17158644u, 0x46e1985eu, - 0x7e1f1d88u, 0x8a48e218u, 0x989e2725u, 0x2d8c4ae2u, 0xe318be2fu, 0x37358e4cu, 0x4ee398deu, 0xfe3f3d90u, - 0x9250e418u, 0x991e4745u, 0x4f4d92e4u, 0x6a25112eu, 0xe6ab29cau, 0xcb6b32bcu, 0x2afcbb2du, 0x31cc6c33u, - 0xf34b3cbbu, 0xdb36b5cau, 0x6716f378u, 0xddd6739cu, 0x9ede6779u, 0xf9fdf67bu, 0x83a0e067u, 0x687a1e16u, - 0x368ba2e2u, 0xe468fa3eu, 0x5e5693a4u, 0x68a6697au, 0xf7777777u, 0x5554fdddu, 0xcb000205u, 0x70817d02u, - 0x00080019u, 0x003f0146u, 0xa41002a4u, 0x3402a602u, 0xaa000a34u, 0xaa100c02u, 0x3402b002u, 0x1502b631u, - 0x400202bcu, 0x02b60101u, 0x8b2ef007u, 0xd74d0005u, 0xe0828001u, 0x00001601u, 0x00000000u, 0x00000000u, - 0x65726162u, 0x746f6f66u, 0x442e4a87u, 0x00000000u, 0x00000001u, 0x00000618u, 0x00000000u, 0x00000156u, - 0x980d163fu, 0xeddd574eu, 0x00000000u, 0x00000000u, +// A single-pass decoupled look-back over one aggregate per block (wave64, one wave per block, lane 0 +// active). The block takes its index from an atomic counter, publishes {flag 1, aggregate} with +// buffer_atomic_swap_x2 (block 0 publishes {2, aggregate}), walks back over earlier blocks with glc +// buffer_load_dwordx2, adding aggregates until it reads an inclusive prefix {2, prefix}, publishes +// {2, prefix + aggregate} and stores its exclusive prefix. A dwordx2 load that sees half of a +// swap_x2 (a new flag with an old value, or an old flag with a new value) gives a wrong prefix. +// s[0:3] counter, s[4:7] aggregates, s[8:11] {flag, value} pairs, s[12:15] prefixes. +alignas(256) constexpr std::array ScanCode{ + 0xbefe0481, // s_mov_b64 exec, 1 + 0x7e020281, // v_mov_b32 v1, 1 + 0x7e000280, // v_mov_b32 v0, 0 + 0xe0c85000, 0x80000100, // buffer_atomic_add v1, v0, s[0:3], 0 offen glc + 0xbf8c3f70, // s_waitcnt vmcnt(0) + 0x7e200501, // v_readfirstlane_b32 s16, v1 + 0x34040282, // v_lshlrev_b32 v2, 2, v1 + 0xe0301000, 0x80010302, // buffer_load_dword v3, v2, s[4:7], 0 offen + 0x34080283, // v_lshlrev_b32 v4, 3, v1 + 0xbf8c3f70, // s_waitcnt vmcnt(0) + 0xbf068010, // s_cmp_eq_u32 s16, 0 + 0x85118182, // s_cselect_b32 s17, 2, 1 + 0x7e0c0211, // v_mov_b32 v6, s17 + 0x7e0e0303, // v_mov_b32 v7, v3 + 0xe1401000, 0x80020604, // buffer_atomic_swap_x2 v[6:7], v4, s[8:11], 0 offen + 0x7e100280, // v_mov_b32 v8, 0 + 0xbf068010, // s_cmp_eq_u32 s16, 0 + 0xbf85000f, // s_cbranch_scc1 publish + 0x80928110, // s_sub_u32 s18, s16, 1 + 0x8f138312, // look: s_lshl_b32 s19, s18, 3 + 0x7e120213, // v_mov_b32 v9, s19 + 0xe0345000, 0x80020a09, // buffer_load_dwordx2 v[10:11], v9, s[8:11], 0 offen glc + 0xbf8c3f70, // s_waitcnt vmcnt(0) + 0x7e28050a, // v_readfirstlane_b32 s20, v10 + 0x7e2a050b, // v_readfirstlane_b32 s21, v11 + 0xbf068014, // s_cmp_eq_u32 s20, 0 + 0xbf85fff7, // s_cbranch_scc1 look + 0x4a101015, // v_add_nc_u32 v8, s21, v8 + 0xbf068214, // s_cmp_eq_u32 s20, 2 + 0xbf850002, // s_cbranch_scc1 publish + 0x80928112, // s_sub_u32 s18, s18, 1 + 0xbf82fff2, // s_branch look + 0x4a1a0708, // publish: v_add_nc_u32 v13, v8, v3 + 0x7e180282, // v_mov_b32 v12, 2 + 0xe1401000, 0x80020c04, // buffer_atomic_swap_x2 v[12:13], v4, s[8:11], 0 offen + 0xe0701000, 0x80030802, // buffer_store_dword v8, v2, s[12:15], 0 offen + 0xbf810000, // s_endpgm }; constexpr std::uint32_t Lanes = 64; -constexpr std::uint32_t PerBlock = 256; -constexpr std::uint32_t Blocks = 125; -constexpr std::uint32_t Elements = 31860; +constexpr std::uint32_t Blocks = 4096; alignas(256) std::array Counter{}; +alignas(256) std::array Aggregates{}; alignas(256) std::array Pairs{}; -alignas(256) std::array Data{}; -alignas(256) std::array Total{}; +alignas(256) std::array Prefixes{}; std::uint32_t Value(std::uint32_t index, std::uint32_t seed) { - return ((index * 2654435761u + seed * 40503u) >> 13u) % 3u; + return ((index * 2654435761u + seed * 40503u) >> 13u) % 1000u; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { alignas(256) static std::array code{}; code = ScanCode; - const auto address = [](const void* data) { return reinterpret_cast(data); }; - std::vector userData{ - static_cast(address(Counter.data())), static_cast((address(Counter.data()) >> 32u) & 0xffffu) | 0x00040000u, 1u, 0x00005204u, - static_cast(address(Pairs.data())), static_cast(address(Pairs.data()) >> 32u), - static_cast(address(Data.data())), static_cast(address(Data.data()) >> 32u), - static_cast(address(Total.data())), static_cast(address(Total.data()) >> 32u), - PerBlock, Blocks, Elements}; + std::vector userData; + for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) { + userData.insert(userData.end(), descriptor.begin(), descriptor.end()); + } + const auto address = reinterpret_cast(code.data()); const std::span words(code); - const std::array memory{{{address(code.data()), std::as_bytes(words)}}}; + const std::array memory{{{address, std::as_bytes(words)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ - {ShaderStage::Compute, address(code.data()), words, 0, {}}, + {ShaderStage::Compute, address, words, 0, {}}, {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, groups, 1, 1, {}, address(code.data())); + device.Dispatch(result, groups, 1, 1, {}, address); device.WaitIdle(); } -std::uint32_t BlockSum(const std::vector& input, std::uint32_t block) { - std::uint32_t sum = 0; - for (std::uint32_t i = block * PerBlock; i < (block + 1) * PerBlock && i < Elements; ++i) sum += input[i]; - return sum; -} - +// One block against published earlier blocks: aggregates (flag 1) except block 0's inclusive prefix. void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { const auto name = "look-back from block " + std::to_string(block); - std::vector input(Data.size(), 0u); - for (std::uint32_t i = 0; i < Elements; ++i) input[i] = Value(i, block); Pairs.fill(0u); std::uint32_t prefix = 0; + for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block); for (std::uint32_t b = 0; b < block; ++b) { - const auto sum = BlockSum(input, b); - prefix += sum; + prefix += Aggregates[b]; Pairs[2 * b] = b == 0 ? 2u : 1u; - Pairs[2 * b + 1] = b == 0 ? prefix : sum; + Pairs[2 * b + 1] = b == 0 ? prefix : Aggregates[b]; } Counter.fill(0u); Counter[0] = block; - std::copy(input.begin(), input.end(), Data.begin()); - Total.fill(0xdeadbeefu); + Prefixes.fill(0xdeadbeefu); Run(device, 1); Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0])); + Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix)); Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2"); - Require(Pairs[2 * block + 1] == prefix + BlockSum(input, block), name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + BlockSum(input, block)) + " (prefix " + std::to_string(prefix) + ")"); - std::uint32_t running = prefix; - for (std::uint32_t i = block * PerBlock; i < (block + 1) * PerBlock && i < Elements; ++i) { - Require(Data[i] == running, name + ": element " + std::to_string(i) + " is " + std::to_string(Data[i]) + ", expected " + std::to_string(running) + " (prefix " + std::to_string(prefix) + ")"); - running += input[i]; - } - if (block == Blocks - 1) Require(Total[0] == running, name + ": the total is " + std::to_string(Total[0]) + ", expected " + std::to_string(running)); + Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block])); } void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) { const auto name = "whole scan " + std::to_string(seed); - std::vector input(Data.size(), 0u); - for (std::uint32_t i = 0; i < Elements; ++i) input[i] = Value(i, seed); + for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed); Counter.fill(0u); Pairs.fill(0u); - std::copy(input.begin(), input.end(), Data.begin()); - Total.fill(0xdeadbeefu); + Prefixes.fill(0xdeadbeefu); Run(device, Blocks); std::uint32_t running = 0; - for (std::uint32_t i = 0; i < Elements; ++i) { - Require(Data[i] == running, name + ": element " + std::to_string(i) + " (block " + std::to_string(i / PerBlock) + ") is " + std::to_string(Data[i]) + ", expected " + std::to_string(running)); - running += input[i]; + for (std::uint32_t b = 0; b < Blocks; ++b) { + Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running)); + running += Aggregates[b]; + Require(Pairs[2 * b] == 2u && Pairs[2 * b + 1] == running, name + ": block " + std::to_string(b) + " published {" + std::to_string(Pairs[2 * b]) + ", " + std::to_string(Pairs[2 * b + 1]) + "}, expected {2, " + std::to_string(running) + "}"); } - Require(Total[0] == running, name + ": the total is " + std::to_string(Total[0]) + ", expected " + std::to_string(running)); } } @@ -179,7 +147,7 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - for (const std::uint32_t block : {0u, 1u, 50u, 63u, 64u, 65u, 100u, 124u}) CheckLookBack(*device, block); + for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block); for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed); std::puts("look-back scan tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 57ce5607d..6dc3ce18d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -227,6 +227,7 @@ Throughout the project, every function at every stage either **does exactly what - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. - [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). +- [Raw `buffer_load_dwordx2`](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped From 9e7c59962e4745445a68477a6d9dea27bfe42f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 11:09:17 +0200 Subject: [PATCH 178/203] test(recompiler): drop the comments in the look-back scan test Co-Authored-By: Claude --- .../tests/execution/LookBackScan.cpp | 51 +++---------------- 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp index e36b005c0..48e3d4346 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -14,51 +14,13 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -// A single-pass decoupled look-back over one aggregate per block (wave64, one wave per block, lane 0 -// active). The block takes its index from an atomic counter, publishes {flag 1, aggregate} with -// buffer_atomic_swap_x2 (block 0 publishes {2, aggregate}), walks back over earlier blocks with glc -// buffer_load_dwordx2, adding aggregates until it reads an inclusive prefix {2, prefix}, publishes -// {2, prefix + aggregate} and stores its exclusive prefix. A dwordx2 load that sees half of a -// swap_x2 (a new flag with an old value, or an old flag with a new value) gives a wrong prefix. -// s[0:3] counter, s[4:7] aggregates, s[8:11] {flag, value} pairs, s[12:15] prefixes. alignas(256) constexpr std::array ScanCode{ - 0xbefe0481, // s_mov_b64 exec, 1 - 0x7e020281, // v_mov_b32 v1, 1 - 0x7e000280, // v_mov_b32 v0, 0 - 0xe0c85000, 0x80000100, // buffer_atomic_add v1, v0, s[0:3], 0 offen glc - 0xbf8c3f70, // s_waitcnt vmcnt(0) - 0x7e200501, // v_readfirstlane_b32 s16, v1 - 0x34040282, // v_lshlrev_b32 v2, 2, v1 - 0xe0301000, 0x80010302, // buffer_load_dword v3, v2, s[4:7], 0 offen - 0x34080283, // v_lshlrev_b32 v4, 3, v1 - 0xbf8c3f70, // s_waitcnt vmcnt(0) - 0xbf068010, // s_cmp_eq_u32 s16, 0 - 0x85118182, // s_cselect_b32 s17, 2, 1 - 0x7e0c0211, // v_mov_b32 v6, s17 - 0x7e0e0303, // v_mov_b32 v7, v3 - 0xe1401000, 0x80020604, // buffer_atomic_swap_x2 v[6:7], v4, s[8:11], 0 offen - 0x7e100280, // v_mov_b32 v8, 0 - 0xbf068010, // s_cmp_eq_u32 s16, 0 - 0xbf85000f, // s_cbranch_scc1 publish - 0x80928110, // s_sub_u32 s18, s16, 1 - 0x8f138312, // look: s_lshl_b32 s19, s18, 3 - 0x7e120213, // v_mov_b32 v9, s19 - 0xe0345000, 0x80020a09, // buffer_load_dwordx2 v[10:11], v9, s[8:11], 0 offen glc - 0xbf8c3f70, // s_waitcnt vmcnt(0) - 0x7e28050a, // v_readfirstlane_b32 s20, v10 - 0x7e2a050b, // v_readfirstlane_b32 s21, v11 - 0xbf068014, // s_cmp_eq_u32 s20, 0 - 0xbf85fff7, // s_cbranch_scc1 look - 0x4a101015, // v_add_nc_u32 v8, s21, v8 - 0xbf068214, // s_cmp_eq_u32 s20, 2 - 0xbf850002, // s_cbranch_scc1 publish - 0x80928112, // s_sub_u32 s18, s18, 1 - 0xbf82fff2, // s_branch look - 0x4a1a0708, // publish: v_add_nc_u32 v13, v8, v3 - 0x7e180282, // v_mov_b32 v12, 2 - 0xe1401000, 0x80020c04, // buffer_atomic_swap_x2 v[12:13], v4, s[8:11], 0 offen - 0xe0701000, 0x80030802, // buffer_store_dword v8, v2, s[12:15], 0 offen - 0xbf810000, // s_endpgm + 0xbefe0481, 0x7e020281, 0x7e000280, 0xe0c85000, 0x80000100, 0xbf8c3f70, 0x7e200501, 0x34040282, + 0xe0301000, 0x80010302, 0x34080283, 0xbf8c3f70, 0xbf068010, 0x85118182, 0x7e0c0211, 0x7e0e0303, + 0xe1401000, 0x80020604, 0x7e100280, 0xbf068010, 0xbf85000f, 0x80928110, 0x8f138312, 0x7e120213, + 0xe0345000, 0x80020a09, 0xbf8c3f70, 0x7e28050a, 0x7e2a050b, 0xbf068014, 0xbf85fff7, 0x4a101015, + 0xbf068214, 0xbf850002, 0x80928112, 0xbf82fff2, 0x4a1a0708, 0x7e180282, 0xe1401000, 0x80020c04, + 0xe0701000, 0x80030802, 0xbf810000, }; constexpr std::uint32_t Lanes = 64; @@ -101,7 +63,6 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { device.WaitIdle(); } -// One block against published earlier blocks: aggregates (flag 1) except block 0's inclusive prefix. void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { const auto name = "look-back from block " + std::to_string(block); Pairs.fill(0u); From cc4d8ec07ee52e12af7348870e98080e9da96d85 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 09:19:01 +0000 Subject: [PATCH 179/203] refactor(recompiler): dispatch image atomics through ImageOpcodeInfoOf EmitImage listed all 27 image atomic opcodes by hand to reach EmitAtomicOp, although it already looks up ImageOpcodeInfoOf for the instruction. Route every opcode whose access is ImageAccess::Atomic to EmitAtomicOp before the switch; the set of opcodes is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017pTZqAyVSZy2FZmaqakPHZ --- .../SpirvBackend/src/SpirvImageEmitter.cpp | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index b0718b50e..5cc1c94c4 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1209,6 +1209,10 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { ctx.Fail(inst, "has no image address"); } const ImageEmitAccess access{inst, mem, image, *address, TableSlot(ctx, inst, mem, image)}; + if (imageInfo.access == ImageAccess::Atomic) { + EmitAtomicOp(ctx, access); + return; + } switch (irOpcode) { case IrOpcode::ImageQueryDimensions: EmitQueryDimensionsOp(ctx, access); @@ -1226,35 +1230,6 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageGatherRaw: EmitSamplingOp(ctx, access); return; - case IrOpcode::ImageAtomicSwap32: - case IrOpcode::ImageAtomicIAdd32: - case IrOpcode::ImageAtomicUMin32: - case IrOpcode::ImageAtomicUMax32: - case IrOpcode::ImageAtomicAnd32: - case IrOpcode::ImageAtomicOr32: - case IrOpcode::ImageAtomicXor32: - case IrOpcode::ImageAtomicCmpSwap32: - case IrOpcode::ImageAtomicISub32: - case IrOpcode::ImageAtomicSMin32: - case IrOpcode::ImageAtomicSMax32: - case IrOpcode::ImageAtomicInc32: - case IrOpcode::ImageAtomicDec32: - case IrOpcode::ImageAtomicFCmpSwap32: - case IrOpcode::ImageAtomicFMin32: - case IrOpcode::ImageAtomicFMax32: - case IrOpcode::ImageAtomicSwap64: - case IrOpcode::ImageAtomicIAdd64: - case IrOpcode::ImageAtomicISub64: - case IrOpcode::ImageAtomicUMin64: - case IrOpcode::ImageAtomicUMax64: - case IrOpcode::ImageAtomicSMin64: - case IrOpcode::ImageAtomicSMax64: - case IrOpcode::ImageAtomicAnd64: - case IrOpcode::ImageAtomicOr64: - case IrOpcode::ImageAtomicXor64: - case IrOpcode::ImageAtomicCmpSwap64: - EmitAtomicOp(ctx, access); - return; default: ctx.Fail(inst, "has no image SPIR-V emitter"); } From 2eebe84f25bd76f4822be070d486edf7f7838038 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 14:02:11 +0000 Subject: [PATCH 180/203] feat(agc): accept texel offsets, LOD biases and LOD clamps in color comparison sampling Comparison sampling of a color texture threw on the _o, _b and _cl forms and on a sampler LOD bias. The emulation fetches the texels itself, so the 6-bit offsets of the address are added to the point or bilinear texel indices, as on the hardware and in Vulkan. The emulation runs only at level zero or on a single-level view, where an LOD bias or clamp cannot select another level, so they are accepted with no effect. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/EmulatedColorCompare.cpp | 53 ++++++++++++++----- .../Optimization/src/ResourceMaterializer.cpp | 3 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 17 ++++-- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 92443d6f9..c664f1d29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -5,6 +5,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -43,6 +44,7 @@ constexpr std::array Red{0, 64, 128, 255}; alignas(256) std::array Input{}; alignas(256) std::array Output{}; +alignas(256) std::array Extra{}; alignas(4096) std::array Texels{}; alignas(256) constexpr std::array Code{ @@ -50,11 +52,17 @@ alignas(256) constexpr std::array Code{ 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, }; +alignas(256) constexpr std::array OffsetCode{ + 0x1614008c, 0xe03c1000, 0x8000030a, 0xe03c1000, 0x8005060a, 0xbf8c3f70, 0x7e020306, 0x7e040307, + 0x7e0c0308, 0xf0f80108, 0x00820901, 0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001090b, 0xbf810000, +}; + struct Sampler { std::uint32_t clamp; std::uint32_t filter; std::uint32_t border = BorderBlack; std::uint32_t reduction = 0; + std::uint32_t lodBias = 0; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -77,7 +85,7 @@ std::array SamplerDescriptor(const Sampler& sampler) { return { sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u) | (sampler.reduction << 29u), 0u, - (sampler.filter << 20u) | (sampler.filter << 22u), + sampler.lodBias | (sampler.filter << 20u) | (sampler.filter << 22u), sampler.border << 30u, }; } @@ -109,6 +117,19 @@ void FillInput() { Input[6] = 0.6f; Input[7] = -0.1f; Input[8] = 0.5f; Input[9] = 0.3f; Input[10] = 0.5f; Input[11] = 1.1f; Input[12] = 0.9f; Input[13] = 1.05f; Input[14] = -0.05f; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = static_cast(tid % 7u) - 3; + const auto y = static_cast((tid / 7u) % 5u) - 2; + Extra[tid * 3u + 0u] = (static_cast(x) & 0x3fu) | ((static_cast(y) & 0x3fu) << 8u) | (tid % 3u == 0u ? 0x3f0000u : 0u); + Extra[tid * 3u + 1u] = std::bit_cast(-2.5f + 0.25f * static_cast(tid % 21u)); + Extra[tid * 3u + 2u] = std::bit_cast(0.5f * static_cast(tid % 5u)); + } + Extra[0] = 0x00001f20u; +} + +std::int32_t OffsetComponent(std::uint32_t tid, std::uint32_t component) { + const auto field = (Extra[tid * 3u] >> (component * 8u)) & 0x3fu; + return static_cast(field ^ 0x20u) - 0x20; } float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) { @@ -127,15 +148,17 @@ float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) { return reference <= red ? 1.0f : 0.0f; } -float Expected(std::uint32_t tid, const Sampler& sampler) { +float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) { const float u = Input[tid * 3u + 1u] * static_cast(Side); const float v = Input[tid * 3u + 2u] * static_cast(Side); const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f); - if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast(std::floor(u)), static_cast(std::floor(v)), reference, sampler); + const int offsetX = offsets ? OffsetComponent(tid, 0u) : 0; + const int offsetY = offsets ? OffsetComponent(tid, 1u) : 0; + if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast(std::floor(u)) + offsetX, static_cast(std::floor(v)) + offsetY, reference, sampler); const float cu = u - 0.5f; const float cv = v - 0.5f; - const int x = static_cast(std::floor(cu)); - const int y = static_cast(std::floor(cv)); + const int x = static_cast(std::floor(cu)) + offsetX; + const int y = static_cast(std::floor(cv)) + offsetY; const float a = cu - std::floor(cu); const float b = cv - std::floor(cv); const float top = ReferenceTexel(x, y, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y, reference, sampler) * a; @@ -143,8 +166,8 @@ float Expected(std::uint32_t tid, const Sampler& sampler) { return top * (1.0f - b) + bottom * b; } -ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler) { - std::vector userData(20, 0u); +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span code = Code) { + std::vector userData(24, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); const auto texture = TextureDescriptor(format); @@ -153,7 +176,8 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: std::copy(output.begin(), output.end(), userData.begin() + 4); std::copy(texture.begin(), texture.end(), userData.begin() + 8); std::copy(samplerWords.begin(), samplerWords.end(), userData.begin() + 16); - const std::span code(Code); + const auto extra = BufferDescriptor(Extra.data(), static_cast(sizeof(Extra))); + std::copy(extra.begin(), extra.end(), userData.begin() + 20); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -166,13 +190,14 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) { +void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false) { Output.fill(-1.0f); - const auto result = Compile(device, Format8888UNorm, sampler); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + const std::span code = offsets ? std::span(OffsetCode) : std::span(Code); + const auto result = Compile(device, Format8888UNorm, sampler, code); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const float expected = Expected(tid, sampler); + const float expected = Expected(tid, sampler, offsets); const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f; Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); } @@ -211,6 +236,10 @@ int main() { Run(*device, {ClampBorder, FilterPoint, BorderBlack}, "point, black border"); Run(*device, {ClampBorder, FilterBilinear, BorderWhite}, "bilinear, white border"); Run(*device, {ClampBorder, FilterBilinear, BorderBlack}, "bilinear, black border"); + Run(*device, {ClampEdge, FilterPoint, BorderBlack, 0u, 0x0140u}, "point, clamp to edge, sampler LOD bias"); + Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge, offsets, bias and LOD clamp", true); + Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap, offsets, bias and LOD clamp", true); + Run(*device, {ClampBorder, FilterBilinear, BorderWhite, 0u, 0x3f00u}, "bilinear, white border, offsets, bias and LOD clamp", true); Reject(*device, Format8888UInt, {ClampEdge, FilterPoint}, "unsupported format"); Reject(*device, Format8888UNorm, {ClampMirror, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 2c46cb0a7..f0d7987b0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -540,7 +540,6 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna const auto function = (words[0] >> 12u) & 0x7u; const bool unnormalized = ((words[0] >> 15u) & 0x1u) != 0u; if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); - const auto lodBias = words[2] & 0x3fffu; const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; const auto addressMode = [](std::uint32_t clamp) { @@ -555,7 +554,7 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna const bool border = addressX == EmulatedCompare::AddressBorder || addressY == EmulatedCompare::AddressBorder; if (border && borderType == 3u) throw std::runtime_error("comparison sampling of a color texture with a border color table is not implemented"); if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("comparison sampling of a color texture is implemented only with equal point or bilinear minification and magnification filters"); - if (unnormalized || lodBias != 0u) throw std::runtime_error("comparison sampling of a color texture does not implement unnormalized coordinates or an LOD bias"); + if (unnormalized) throw std::runtime_error("comparison sampling of a color texture does not implement unnormalized coordinates"); const auto state = EmulatedCompare::Enabled | (function << EmulatedCompare::FunctionShift) | (magFilter == 1u ? EmulatedCompare::Linear : 0u) | (addressX << EmulatedCompare::ClampXShift) | (addressY << EmulatedCompare::ClampYShift) | (border && borderType == 2u ? EmulatedCompare::BorderWhite : 0u); if (samplerState.has_value() && *samplerState != state) throw std::runtime_error("comparison sampling of a color texture through samplers that disagree is not implemented"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index eb77daa6d..33a933301 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -970,7 +970,6 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto compare = image.emulatedCompare; if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented"); if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); - if (setup.layout.bias != NoImageComponent || setup.layout.offset != NoImageComponent || setup.layout.clamp != NoImageComponent) ctx.Fail(access.inst, "compares against a color texture with an LOD bias, texel offset or LOD clamp, which is not implemented"); if (!HasFlag(mem, RdnaImageSampleFlagLevelZero) && (compare & EmulatedCompare::SingleLevel) == 0u) ctx.Fail(access.inst, "compares against a color texture across mip levels, which is not implemented"); const bool arrayed = image.dimension == RdnaImageDimension::Dim2DArray; if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, "compares against a color texture that is not a 2D or 2D array view, which is not implemented"); @@ -1048,9 +1047,19 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess }; const auto scaledU = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); const auto scaledV = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); + auto offsetX = ConstantI32(state, 0); + auto offsetY = ConstantI32(state, 0); + if (setup.layout.offset != NoImageComponent) { + const auto offset = PackedOffset(ctx, access, setup.layout); + offsetX = extract(i32, offset, 0u); + offsetY = extract(i32, offset, 1u); + } + const auto texelIndex = [&](std::uint32_t coordinate, std::uint32_t offset) { + return Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {coordinate})), offset); + }; std::uint32_t result; if ((compare & EmulatedCompare::Linear) == 0u) { - result = compareTexel(Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledU})), Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledV}))); + result = compareTexel(texelIndex(scaledU, offsetX), texelIndex(scaledV, offsetY)); } else { const auto half = f32Constant(0.5f); const auto centreU = Binary(state, spv::OpFSub, f32, scaledU, half); @@ -1059,8 +1068,8 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto floorV = ext(f32, GLSLstd450Floor, {centreV}); const auto weightU = Binary(state, spv::OpFSub, f32, centreU, floorU); const auto weightV = Binary(state, spv::OpFSub, f32, centreV, floorV); - const auto x0 = Unary(state, spv::OpConvertFToS, i32, floorU); - const auto y0 = Unary(state, spv::OpConvertFToS, i32, floorV); + const auto x0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorU), offsetX); + const auto y0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorV), offsetY); const auto x1 = Binary(state, spv::OpIAdd, i32, x0, ConstantI32(state, 1)); const auto y1 = Binary(state, spv::OpIAdd, i32, y0, ConstantI32(state, 1)); const auto top = ext(f32, GLSLstd450FMix, {compareTexel(x0, y0), compareTexel(x1, y0), weightU}); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6dc3ce18d..f5b3032d5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -220,7 +220,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. -- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented; half-border addressing, the border color table, gradients, LOD bias, offsets, LOD clamps, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. - [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. From 5582c993e2f3cde46d765bf664c53559e97c4f93 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2026 15:20:53 -0500 Subject: [PATCH 181/203] feat(shader): implement ds_wrxchg2_rtn_b64 and ds_wrxchg2st64_rtn_b64 DS opcodes 0x6e and 0x6f exchange two qwords at offset0 and offset1 scaled by 8 (st64: 512) and return both old values. Each address is exchanged with its own 64-bit LDS exchange on the locked 64-bit path. Equal offsets and a destination past v255 throw, as for the b32 forms; 64-bit GDS throws like the other 64-bit DS atomics. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/DsWrxchg2B64.cpp | 136 ++++++++++++++++++ .../include/RdnaDecoder/RdnaOpcode.hpp | 2 + .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 12 +- .../Translation/src/InstructionTranslator.cpp | 4 +- .../MemoryInstructionDispatch.cpp | 2 + .../SharedMemoryInstructions.cpp | 21 +-- docs/dev/TechnicalDebt.md | 1 + 7 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp new file mode 100644 index 000000000..b07927ea4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp @@ -0,0 +1,136 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t LdsDwords = 3072; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr auto Code = std::to_array({ + 0x34020085, 0xe0381000, 0x80000201, 0xe0381010, 0x80000601, + 0x345c0085, 0x4a5c5cff, 0x00000480, 0x345e0083, 0x4a5e5eff, 0x00000880, + 0xbf8c3f70, + 0xd9380301, 0x0004022e, 0xd9b80301, 0x1208062e, 0xd9dc0301, 0x1600002e, + 0xd93c0100, 0x0004022f, 0xd9bc0100, 0x1a08062f, 0xd9e00100, 0x1e00002f, + 0x34020086, + 0xbf8cc07f, + 0xe0781000, 0x80011201, 0xe0781010, 0x80011601, 0xe0781020, 0x80011a01, 0xe0781030, 0x80011e01, + 0xbf810000, +}); + +alignas(256) constexpr auto CoincidingOffsets = std::to_array({0xd9b80101, 0x1208062e, 0xbf810000}); +alignas(256) constexpr auto CoincidingOffsetsSt64 = std::to_array({0xd9bc0000, 0x1a08062f, 0xbf810000}); +alignas(256) constexpr auto DestinationOverflow = std::to_array({0xd9b80301, 0xfd08062e, 0xbf810000}); + +void Fill(std::uint32_t tid, std::uint32_t* words) { + std::uint64_t seed = (tid + 1u) * 0x9e3779b97f4a7c15ull; + for (std::uint32_t i = 0; i < Inputs; ++i) { + seed = seed * 0xbf58476d1ce4e5b9ull + 0x94d049bb133111ebull; + words[i] = static_cast(seed >> 32u); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +template +void Translate(AgcDriver::VulkanDevice& device, std::span code, TUse&& use) { + 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}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + use(ShaderRecompiler::Recompile(request)); +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + Translate(device, Code, [&](const auto& result) { + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + }); +} + +void Reject(AgcDriver::VulkanDevice& device, std::span code, const std::string& reason) { + std::string error; + try { + Translate(device, code, [](const auto&) {}); + } catch (const std::exception& exception) { + error = exception.what(); + } + Require(error.find(reason) != std::string::npos, "ds wrxchg2 b64: expected a rejection for \"" + reason + "\", got \"" + error + "\""); +} + +void CheckRejections(AgcDriver::VulkanDevice& device) { + Reject(device, CoincidingOffsets, "DS write exchange of one location through both offsets is not supported"); + Reject(device, CoincidingOffsetsSt64, "DS write exchange of one location through both offsets is not supported"); + Reject(device, DestinationOverflow, "DS write exchange destination register range overflow"); +} + +void Check() { + constexpr std::array names{"ds_wrxchg2_rtn_b64", "ds_wrxchg2st64_rtn_b64"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t op = 0; op < 2u; ++op) { + for (std::uint32_t i = 0; i < 4u; ++i) { + const auto returned = out[op * 8u + i]; + const auto stored = out[op * 8u + 4u + i]; + Require(returned == in[i], std::string("ds wrxchg2 b64: lane ") + std::to_string(tid) + " " + names[op] + " returned dword " + std::to_string(i) + " is " + Hex(returned) + ", expected " + Hex(in[i])); + Require(stored == in[4u + i], std::string("ds wrxchg2 b64: lane ") + std::to_string(tid) + " " + names[op] + " stored dword " + std::to_string(i) + " is " + Hex(stored) + ", expected " + Hex(in[4u + i])); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + CheckRejections(*device); + std::puts("ds wrxchg2 b64 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index 5899f0335..9cc00be2f 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -861,6 +861,8 @@ enum class RdnaOpcode : std::uint16_t { DsWrxchgRtnB32, DsWrxchg2RtnB32, DsWrxchg2st64RtnB32, + DsWrxchg2RtnB64, + DsWrxchg2st64RtnB64, DsMinF32, DsMaxF32, DsSwizzleB32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index f8c180d05..3f155040a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -319,6 +319,8 @@ constexpr MemoryOpcodeInfo dsOpcodes[] = { {0x4du, RdnaOpcode::DsWriteB64, 2, 32, false, false, false}, {0x4eu, RdnaOpcode::DsWrite2B64, 4, 32, false, false, false}, {0x4fu, RdnaOpcode::DsWrite2st64B64, 4, 32, false, false, false}, + {0x6eu, RdnaOpcode::DsWrxchg2RtnB64, 4, 32, false, false, false}, + {0x6fu, RdnaOpcode::DsWrxchg2st64RtnB64, 4, 32, false, false, false}, {0x76u, RdnaOpcode::DsReadB64, 2, 32, false, false, false}, {0x77u, RdnaOpcode::DsRead2B64, 4, 32, false, false, false}, {0x78u, RdnaOpcode::DsRead2st64B64, 4, 32, false, false, false}, @@ -535,6 +537,8 @@ std::uint32_t dsSourceCount(RdnaOpcode opcode) { switch (opcode) { case RdnaOpcode::DsWrxchg2RtnB32: case RdnaOpcode::DsWrxchg2st64RtnB32: + case RdnaOpcode::DsWrxchg2RtnB64: + case RdnaOpcode::DsWrxchg2st64RtnB64: case RdnaOpcode::DsWrite2B32: case RdnaOpcode::DsWrite2st64B32: case RdnaOpcode::DsWrite2B64: @@ -903,11 +907,11 @@ RdnaInstruction DecodeRdnaDs(std::uint32_t programCounter, std::span 256u) { throw std::runtime_error("DS write exchange destination register range overflow"); } } @@ -933,10 +937,10 @@ RdnaInstruction DecodeRdnaDs(std::uint32_t programCounter, std::span Date: Wed, 7 Oct 2026 11:40:52 +0200 Subject: [PATCH 182/203] docs: describe the ds_wrxchg2 b64 forms in the measured b32 entry Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5b5005d6b..ea5145962 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -192,7 +192,7 @@ Throughout the project, every function at every stage either **does exactly what - [Unnormalized S#s](../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without offset, comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - An [unnormalized S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. -- [`ds_wrxchg2_rtn_b32` and `ds_wrxchg2st64_rtn_b32`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. +- [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [64-bit image atomics](../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. @@ -219,7 +219,6 @@ Throughout the project, every function at every stage either **does exactly what - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. -- [ds_wrxchg2(st64)_rtn_b64](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) exchange the two qwords as two separate 64-bit LDS exchanges, first then second: another lane can observe the first address written before the second. Whether the hardware exchanges the pair atomically is not documented. Equal offsets throw, as for the b32 forms - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. - [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented; half-border addressing, the border color table, gradients, LOD bias, offsets, LOD clamps, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. From 91852719bf928a38b5618773c059b6fa2d4e7e4c Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:46:17 +0200 Subject: [PATCH 183/203] feat(agc): implement 8 and 8_8 srgb image loads through unorm views NVIDIA 610.62 cannot sample R8G8_SRGB, and lavapipe can sample neither it nor R8_SRGB, so an 8_8 sRGB T# made an image the device does not support: creating it crashes the NVIDIA driver and lavapipe loads zeros. The device's format support is read once. An 8 or 8_8 sRGB texture the device cannot sample is created and viewed as R8_UNORM or R8G8_UNORM, and SpirvTarget::srgbDecodeFormats makes the materializer mark the image, so image loads map every channel through a 256-entry table of the sRGB EOTF rounded to nearest even at bf16 precision, which is what RDNA2 (gfx1035) returns; both channels of 8_8 are sRGB, and DST_SEL 0 and 1 map to themselves. Samples and gathers of such an image throw, and its bindless table entries are unmapped like converted formats. APS5_SRGB_SHADER_DECODE=1 takes this path on every device. Shader disk cache entries and keys carry the mark: cache format version 7. --- .../Execution/src/VulkanDevice.cpp | 5 + .../Graphics/include/Context.hpp | 1 + .../Graphics/include/TextureFormat.hpp | 4 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 +- .../Graphics/src/TextureFormat.cpp | 24 ++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 3 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 16 +- .../libSceAgcDriver/tests/TextureFormat.cpp | 33 +++ .../tests/execution/ImageSrgbLoad.cpp | 242 ++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 2 + core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../IrMetadata/ResourcePlan.hpp | 1 + .../IrMetadata/Resources.hpp | 1 + .../Optimization/ResourceMaterializer.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 15 +- .../RdnaDecoder/RdnaDescriptorFormat.hpp | 16 ++ core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 5 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 66 +++++ docs/dev/TechnicalDebt.md | 4 + 24 files changed, 440 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a3c85be11..cbd112add 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/General.hpp" #ifdef _WIN32 @@ -197,6 +198,7 @@ struct VulkanDevice::State { bool imageViewMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = false; + std::uint32_t srgbDecodeFormats = 0; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; @@ -682,6 +684,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); state->properties = properties.properties; state->physical = selected; + state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) { state->capabilities.push_back(spv::CapabilityGroupNonUniform); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot); @@ -2360,6 +2363,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}}; target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric; target.nonConstantImageOffsets = state->maintenance8; + target.srgbDecodeFormats = state->srgbDecodeFormats; if (state->meshShader) { const auto& mesh = state->meshLimits; target.mesh = ShaderRecompiler::MeshTargetLimits{{mesh.maxMeshWorkGroupSize[0], mesh.maxMeshWorkGroupSize[1], mesh.maxMeshWorkGroupSize[2]}, mesh.maxMeshWorkGroupInvocations, std::min(mesh.maxMeshSharedMemorySize, mesh.maxMeshPayloadAndSharedMemorySize), mesh.maxMeshOutputVertices, mesh.maxMeshOutputPrimitives, mesh.maxMeshOutputComponents, std::min(mesh.maxMeshOutputMemorySize, mesh.maxMeshPayloadAndOutputMemorySize), mesh.meshOutputPerVertexGranularity, mesh.meshOutputPerPrimitiveGranularity}; @@ -2429,6 +2433,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; + context.srgbDecodeFormats = state->srgbDecodeFormats; return context; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index eebe5ff7f..842d3230f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -138,6 +138,7 @@ struct Context { bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; + std::uint32_t srgbDecodeFormats = 0; template TFunction Function(const char* name) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index aca377fc8..8820c85bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -10,7 +10,11 @@ namespace AgcDriver::Graphics { +struct Context; + VkFormat ResolveTextureFormat(std::uint32_t guestFormat); +std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); +VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7e30db5e6..32e9fea77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -229,7 +229,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; try { - const auto colorFormat = ResolveTextureFormat(descriptor.format); + const auto colorFormat = SampledTextureFormat(context, descriptor.format); Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture"); Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures"); const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat; @@ -456,7 +456,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& PhaseTimer timer; try { Require(source != nullptr && CanCopyFrom(*source, descriptor), "storage image does not match the sampled texture"); - const auto vkFormat = ResolveTextureFormat(descriptor.format); + const auto vkFormat = SampledTextureFormat(context, descriptor.format); const auto geometry = DescribeSurface(descriptor); APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u viewed from storage image (vk=%d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast(vkFormat)); // Storage images stay in the general layout; the view samples them there. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 108bdbe5d..88a1df2a7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include #include #include @@ -108,6 +111,27 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { return findFormatEntry(guestFormat).vkFormat; } +std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + const bool always = forced != nullptr && std::strcmp(forced, "1") == 0; + const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) { + VkFormatProperties properties{}; + formatProperties(physical, ResolveTextureFormat(static_cast(format)), &properties); + return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0; + }; + std::uint32_t formats = 0; + for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) { + if ((always || !sampled(format)) && sampled(ShaderRecompiler::SrgbUnormFormat(format))) formats |= ShaderRecompiler::SrgbDecodeBit(format); + } + return formats; +} + +VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) { + const auto format = static_cast(guestFormat); + if ((context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) return ResolveTextureFormat(guestFormat); + return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); +} + std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) { for (const auto& entry : kFormatLookup) { if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 0adf42ad9..6752641fb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -162,6 +162,7 @@ CompiledVariant sampleVariant() { image.mipCount = 4; image.shaderSwizzle = 0x321u; image.written = true; + image.srgbDecode = true; image.cube = true; image.r128 = true; image.indirectRoot = 0; @@ -329,6 +330,7 @@ void verifyKeySensitivity() { changes("an extension", [](SampleRequest& sample) { sample.extensions[0] = "SPV_KHR_storage_buffer_storage_clasS"; }); changes("barycentrics", [](SampleRequest& sample) { sample.request.target.fragmentShaderBarycentricEnabled = true; }); changes("non-constant texel offsets", [](SampleRequest& sample) { sample.request.target.nonConstantImageOffsets = true; }); + changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; }); 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; }); @@ -351,6 +353,7 @@ void verifyKeySensitivity() { changes("an image search depth", [](SampleRequest& sample) { sample.specialization.images[0].indirectSearchIterations = 2; }); changes("an image cube flag", [](SampleRequest& sample) { sample.specialization.images[0].cube = true; }); changes("an image FMASK flag", [](SampleRequest& sample) { sample.specialization.images[0].fmask = true; }); + changes("an image sRGB decode", [](SampleRequest& sample) { sample.specialization.images[0].srgbDecode = true; }); changes("the image count", [](SampleRequest& sample) { sample.specialization.images.emplace_back(); }); changes("the bound descriptors", [](SampleRequest& sample) { sample.specialization.boundDescriptors.push_back(1); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 75562d705..1fb63361b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -702,19 +702,19 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQQgAAAA=", "new requests did not use serialization version 8"); + require(requestPrefix(encoded, 8u) == "NVNQQQkAAAA=", "new requests did not use serialization version 9"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 162u; for (std::size_t bytes = 0; bytes < 8u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-8 pixel mapping was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQkAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQoAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; - static constexpr std::array legacyPixelRequests{ + static constexpr std::array legacyPixelRequests{ "NVNQQQEAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" @@ -750,6 +750,11 @@ void verifyLegacyPixelRequests() { "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" "AAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAA" "AIAAAAAAAAE=", + "NVNQQQgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAsAAAAAAAAAgAAAAAAAAQ==", }; const RequestSerializer serializer; for (std::size_t index = 0; index < legacyPixelRequests.size(); ++index) { @@ -763,7 +768,7 @@ void verifyLegacyPixelRequests() { require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); - require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u), "legacy request trailer was misread"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u, "legacy request trailer was misread"); const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); } @@ -1508,6 +1513,9 @@ int main() { auto offsets = uncached; offsets.target.nonConstantImageOffsets = true; require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(offsets)).request.target.nonConstantImageOffsets, "non-constant texel offsets were lost in serialization"); + auto srgb = uncached; + srgb.target.srgbDecodeFormats = 3u; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(srgb)).request.target.srgbDecodeFormats == 3u, "the sRGB formats decoded in the shader were lost in serialization"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 3973bfa18..a755e0206 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -12,6 +12,38 @@ namespace { using namespace AgcDriver::Graphics; +VkFormat unsampledFormats[2]{}; + +void unsampledFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format != unsampledFormats[0] && format != unsampledFormats[1]) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; +} + +std::uint32_t decodedWithout(VkFormat first = VK_FORMAT_UNDEFINED, VkFormat second = VK_FORMAT_UNDEFINED) { + unsampledFormats[0] = first; + unsampledFormats[1] = second; + return SrgbDecodeFormats(unsampledFormatProperties, VK_NULL_HANDLE); +} + +void srgbDecodeTests() { + constexpr std::uint32_t srgb8 = 1u; + constexpr std::uint32_t srgb8_8 = 2u; + Require(decodedWithout() == 0u, "a device that samples every sRGB format needs no shader decode"); + Require(decodedWithout(VK_FORMAT_R8G8_SRGB) == srgb8_8, "a device without sampled R8G8_SRGB must decode 8_8_SRGB in the shader"); + Require(decodedWithout(VK_FORMAT_R8_SRGB) == srgb8, "a device without sampled R8_SRGB must decode 8_SRGB in the shader"); + Require(decodedWithout(VK_FORMAT_R8_SRGB, VK_FORMAT_R8G8_SRGB) == (srgb8 | srgb8_8), "a device without either 8-bit sRGB format must decode both in the shader"); + Require(decodedWithout(VK_FORMAT_R8G8_SRGB, VK_FORMAT_R8G8_UNORM) == 0u, "8_8_SRGB without a sampled UNORM view must not be decoded in the shader"); + Require(decodedWithout(VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_BC1_RGBA_SRGB_BLOCK) == 0u, "only the 8 and 8_8 sRGB formats may be decoded in the shader"); + Context context{}; + Require(SampledTextureFormat(context, 129) == VK_FORMAT_R8G8_SRGB && SampledTextureFormat(context, 128) == VK_FORMAT_R8_SRGB, "sRGB textures must keep their sRGB views without shader decode"); + context.srgbDecodeFormats = srgb8_8; + Require(SampledTextureFormat(context, 129) == VK_FORMAT_R8G8_UNORM, "8_8_SRGB decoded in the shader must be viewed as R8G8_UNORM"); + Require(SampledTextureFormat(context, 128) == VK_FORMAT_R8_SRGB, "8_SRGB must keep its sRGB view when only 8_8_SRGB is decoded in the shader"); + context.srgbDecodeFormats = srgb8 | srgb8_8; + Require(SampledTextureFormat(context, 128) == VK_FORMAT_R8_UNORM, "8_SRGB decoded in the shader must be viewed as R8_UNORM"); + Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views"); +} + template void reject(TAction action, std::string_view reason) { try { @@ -43,6 +75,7 @@ void convertedDccClearTests() { } void RunTextureFormatTests() { + srgbDecodeTests(); Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); Require(BytesPerElement(1) == 1u, "format 1 must be one byte wide"); Require(!IsBlockCompressed(1), "format 1 must not be block compressed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp new file mode 100644 index 000000000..d03b3026e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -0,0 +1,242 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Groups = Width / Threads; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Unorm8 = 1; +constexpr std::uint32_t Unorm8_8 = 14; +constexpr std::uint32_t Srgb8 = 128; +constexpr std::uint32_t Srgb8_8 = 129; +constexpr std::uint32_t SwizzleXY01 = 0x22cu; +constexpr std::uint32_t SwizzleYX10 = 0x065u; +constexpr std::uint32_t SwizzleXXX1 = 0x324u; +constexpr std::uint32_t One = 0x3f800000u; +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Output{}; +alignas(256) std::array Texels{}; + +constexpr std::array LoadCode(std::uint32_t dmask) { + return { + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x7e3e0280u, 0x7e140280u, 0x7e160280u, 0x7e180280u, + 0x7e1a0280u, 0xf0001008u | (dmask << 8u), 0x00010a1eu, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, + }; +} + +constexpr std::array StoreCode(std::uint32_t dmask) { + return { + 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x7e3e0280u, 0x7e140d1eu, 0x4c163cffu, 0x000000ffu, 0x7e160d0bu, + 0x101414ffu, 0x3b808081u, 0x101616ffu, 0x3b808081u, 0xf0201008u | (dmask << 8u), 0x00010a1eu, 0xbf810000u, + }; +} + +constexpr std::array SamplerCode(std::uint32_t word0) { + return { + 0x34060084u, 0x7e280280u, 0x7e2a0280u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, + 0xbf810000u, + }; +} + +alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu); +alignas(256) constexpr auto LoadXy = LoadCode(0x3u); +alignas(256) constexpr auto StoreX = StoreCode(0x1u); +alignas(256) constexpr auto StoreXy = StoreCode(0x3u); +alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); +alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image sRGB load: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Linear(std::uint32_t code) { + const double encoded = code / 255.0; + const double linear = encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); + int exponent = 0; + const double mantissa = std::frexp(linear, &exponent); + return std::bit_cast(static_cast(std::ldexp(std::nearbyint(mantissa * 256.0), exponent - 8))); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* texels, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Fill(std::uint32_t format) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (format == Srgb8) { + Texels[x] = static_cast(x); + } else { + Texels[x * 2u] = static_cast(x); + Texels[x * 2u + 1u] = static_cast(255u - x); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto texture = TextureDescriptor(texels, format, swizzle); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Verify(std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { + for (std::uint32_t x = 0; x < Width; ++x) { + const std::array channels{Linear(x), format == Srgb8 ? 0u : Linear(255u - x), 0u, One}; + std::uint32_t slot = 0; + for (std::uint32_t component = 0; component < 4u; ++component) { + if (((dmask >> component) & 1u) == 0u) continue; + const auto selector = (swizzle >> (component * 3u)) & 7u; + const auto expected = selector == 0u ? 0u : selector == 1u ? One : channels[selector - 4u]; + const auto actual = Output[x * 4u + slot++]; + Require(actual == expected, what + ": texel " + std::to_string(x) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +void CheckLoad(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { + Fill(format); + Run(device, code, Texels.data(), format, swizzle, Groups); + Verify(dmask, format, swizzle, what); +} + +void CheckStoredLoad(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { + const auto address = reinterpret_cast(block.Data()); + const auto bytes = Width * (format == Srgb8 ? 1u : 2u); + std::fill(block.Data(), block.Data() + BlockBytes, std::uint8_t{0x5au}); + Run(device, format == Srgb8 ? std::span(StoreX) : std::span(StoreXy), block.Data(), format == Srgb8 ? Unorm8 : Unorm8_8, SwizzleXY01, Groups); + Require(AgcDriver::Graphics::StorageTexture::FindPending(address, bytes) != nullptr, what + ": the image_store results are not pending in the storage image"); + Run(device, code, block.Data(), format, swizzle, Groups); + Verify(dmask, format, swizzle, what); + AgcDriver::Graphics::StorageTexture::FlushPending(address, BlockBytes, nullptr, "test"); + device.WaitIdle(); +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::string& what) { + Fill(Srgb8_8); + std::string refusal; + try { + Run(device, code, Texels.data(), Srgb8_8, SwizzleXY01, 1); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("samples or gathers an sRGB image the device cannot sample") != std::string::npos, what + " of an 8_8_SRGB image read through its UNORM view was not refused: " + refusal); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_SRGB_SHADER_DECODE", "1"); +#else + setenv("APS5_SRGB_SHADER_DECODE", "1", 1); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckLoad(*device, LoadXyzw, 0xfu, Srgb8_8, SwizzleXY01, "8_8_SRGB image_load dmask:0xf X Y 0 1"); + CheckLoad(*device, LoadXyzw, 0xfu, Srgb8_8, SwizzleYX10, "8_8_SRGB image_load dmask:0xf Y X 1 0"); + CheckLoad(*device, LoadXy, 0x3u, Srgb8_8, SwizzleXY01, "8_8_SRGB image_load dmask:0x3 X Y 0 1"); + CheckLoad(*device, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1"); + GuestBlock block; + CheckStoredLoad(*device, block, LoadXy, 0x3u, Srgb8_8, SwizzleXY01, "8_8_SRGB image_load dmask:0x3 X Y 0 1 of 8_8_UNORM image_stores"); + CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8_8, SwizzleYX10, "8_8_SRGB image_load dmask:0xf Y X 1 0 of 8_8_UNORM image_stores"); + CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1 of 8_UNORM image_stores"); + RequireRefused(*device, SampleLz, "image_sample_lz"); + RequireRefused(*device, Gather4Lz, "image_gather4_lz"); + std::puts("image sRGB load tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 50ff3356d..0dceb786f 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -219,6 +219,8 @@ function(add_shader_recompiler target runtimeTarget) ResourceMaterializer::SpecializationNanoseconds SetDebugProbeActive ShaderCacheDirectory + SrgbDecodeBit + SrgbUnormFormat ThreadOwned WriteFileAtomically ) diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 800fdbbbd..368514559 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -201,6 +201,7 @@ private: append(key, value.mesh); append(key, value.tessellation); append(key, value.nonConstantImageOffsets); + append(key, value.srgbDecodeFormats); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index d6c4a5750..a6e1b095c 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -697,7 +697,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(8u); + writer.WriteU32(9u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -713,6 +713,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const } } writer.WriteBool(request.target.nonConstantImageOffsets); + writer.WriteU32(request.target.srgbDecodeFormats); return base64Encode(buffer); } @@ -721,7 +722,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 8u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 9u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -738,6 +739,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } } if (version >= 6u) result.request.target.nonConstantImageOffsets = reader.ReadBool(); + if (version >= 9u) result.request.target.srgbDecodeFormats = reader.ReadU32(); return result; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index 4c23a8014..a59dbab67 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -55,6 +55,7 @@ struct IrResourcePlan { std::uint64_t shaderHash = 0; std::uint32_t userDataBase = 0; std::uint32_t userDataCount = 64; + std::uint32_t srgbDecodeFormats = 0; std::vector> valueStorage; std::vector> blockStorage; std::vector memoryInfo; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index a1cae49d2..a25930f11 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -73,6 +73,7 @@ struct ImageResource { bool depthCompare = false; bool cube = false; bool r128 = false; + bool srgbDecode = false; bool depthBits = false; bool depthUnorm16 = false; bool packed = false; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 92ba14de9..aed4b1d83 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -33,6 +33,7 @@ struct ResourceSpecialization { bool depthUnorm16 = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; std::uint32_t emulatedCompare = 0; + bool srgbDecode = false; bool operator==(const Image& other) const; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index f0d7987b0..dce2a07f6 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -46,6 +46,7 @@ struct DecodedImage { bool depthBits = false; bool depthUnorm16 = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; + bool srgbDecode = false; }; ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { @@ -130,7 +131,7 @@ std::uint32_t storageMipCount(const ImageResource& base, const DescriptorValue& return mipBase <= mipLast ? mipLast - mipBase + 1u : 0u; } -DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base) { +DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base, std::uint32_t srgbDecodeFormats) { DecodedImage decoded; decoded.mipCount = storageMipCount(base, descriptor); if (decoded.mipCount == 0u) { @@ -169,6 +170,7 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.packedFormat = format; } decoded.conversionFormat = RemapTextureFormat(format) != format ? format : IrBufferFormat::Invalid; + decoded.srgbDecode = !storage && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (storage || decoded.conversionFormat != IrBufferFormat::Invalid) { decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); } @@ -379,7 +381,7 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con bool usable = !nullImageDescriptor(candidate) && validImageDescriptor(candidate, image.r128); if (usable) { try { - decoded = decodeImageDescriptor(candidate, image); + decoded = decodeImageDescriptor(candidate, image, plan.srgbDecodeFormats); } catch (const std::exception&) { usable = false; } @@ -388,7 +390,7 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con paddedNull++; continue; } - if (decoded.conversionFormat != IrBufferFormat::Invalid || decoded.fmask) { + if (decoded.conversionFormat != IrBufferFormat::Invalid || decoded.fmask || decoded.srgbDecode) { paddedConversion++; continue; } @@ -593,7 +595,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s result.images.reserve(plan.info.images.size()); for (std::uint32_t i = 0; i < plan.info.images.size(); i++) { const auto& image = plan.info.images[i]; - const DecodedImage decoded = decodeImageDescriptor(snapshot.images[i], image); + const DecodedImage decoded = decodeImageDescriptor(snapshot.images[i], image, plan.srgbDecodeFormats); if (decoded.fmask && std::any_of(plan.info.sampledPairs.begin(), plan.info.sampledPairs.end(), [i](const SampledResourcePair& pair) { return pair.image == i; })) { throw std::runtime_error("FMASK requires a direct image load"); } @@ -612,6 +614,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.depthUnorm16 = decoded.depthUnorm16; entry.packedFormat = decoded.packedFormat; entry.emulatedCompare = emulatedCompareState(plan, snapshot, i); + entry.srgbDecode = decoded.srgbDecode; result.images.push_back(entry); } @@ -700,6 +703,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio image.depthUnorm16 = source.depthUnorm16; image.packedFormat = source.packedFormat; image.emulatedCompare = source.emulatedCompare; + image.srgbDecode = source.srgbDecode; if ((source.emulatedCompare & EmulatedCompare::Enabled) != 0u) image.depthCompare = false; image.indirectResources.clear(); } @@ -948,6 +952,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const plan.shaderHash = source.shaderHash; plan.userDataBase = source.userDataBase; plan.userDataCount = source.userDataCount; + plan.srgbDecodeFormats = source.srgbDecodeFormats; plan.memoryInfo = source.memoryInfo; plan.descriptorSources = source.descriptorSources; plan.controlFlow = source.controlFlow; @@ -1029,7 +1034,7 @@ bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { } bool ResourceSpecialization::Image::operator==(const Image& other) const { - return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && packedFormat == other.packedFormat && emulatedCompare == other.emulatedCompare; + return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && packedFormat == other.packedFormat && emulatedCompare == other.emulatedCompare && srgbDecode == other.srgbDecode; } bool ResourceSpecialization::operator==(const ResourceSpecialization& other) const { diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index c700ee099..32796f010 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -23,6 +23,22 @@ enum class ImageType : std::uint32_t { [[nodiscard]] std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3); [[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); +[[nodiscard]] constexpr std::uint32_t SrgbDecodeBit(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Format8Srgb: return 1u; + case IrBufferFormat::Format8_8Srgb: return 2u; + default: return 0u; + } +} + +[[nodiscard]] constexpr IrBufferFormat SrgbUnormFormat(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Format8Srgb: return IrBufferFormat::Format8UNorm; + case IrBufferFormat::Format8_8Srgb: return IrBufferFormat::Format8_8UNorm; + default: return IrBufferFormat::Invalid; + } +} + } #endif diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 817f4fc7f..132df6d77 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -170,6 +170,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { resourceTracker.Track(program); deadCodeEliminator.Eliminate(program); dumpIr("resources"); + program.Resources().srgbDecodeFormats = request.target.srgbDecodeFormats; return program; } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 87c7daffd..9db87189b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -197,6 +197,7 @@ struct SpirvTarget { std::optional mesh; std::optional tessellation; bool nonConstantImageOffsets = false; + std::uint32_t srgbDecodeFormats = 0; }; struct BindingLayout { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 02aa4983b..ed87d113e 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -80,7 +80,7 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 120, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(ResourceSpecialization) == 72, "ResourceSpecialization changed: update BuildKey"); static_assert(sizeof(ResourceSpecialization::Buffer) == 16, "ResourceSpecialization::Buffer changed: update BuildKey"); -static_assert(sizeof(ResourceSpecialization::Image) == 44, "ResourceSpecialization::Image changed: update BuildKey"); +static_assert(sizeof(ResourceSpecialization::Image) == 48, "ResourceSpecialization::Image changed: update BuildKey"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); #endif @@ -413,6 +413,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.depthCompare); out.Value(image.cube); out.Value(image.r128); + out.Value(image.srgbDecode); out.Value(image.depthBits); out.Value(image.depthUnorm16); out.Value(image.packed); @@ -505,6 +506,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.depthCompare); in.Value(image.cube); in.Value(image.r128); + in.Value(image.srgbDecode); in.Value(image.depthBits); in.Value(image.depthUnorm16); in.Value(image.packed); @@ -827,6 +829,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, c out.Value(image.depthUnorm16); out.Value(image.packedFormat); out.Value(image.emulatedCompare); + out.Value(image.srgbDecode); }); writer.Values(std::span(specialization.boundDescriptors)); const auto& switches = switchKey(); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index a0af13f93..01a2e20bb 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 6; +inline constexpr std::uint32_t FormatVersion = 7; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1e761cf73..0508269dc 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -115,6 +115,7 @@ struct SpirvEmitterState { std::uint32_t loopGuardLimit = 0; std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; + std::uint32_t srgbTableVariable = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 33a933301..fb2204ee7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -327,6 +327,66 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac return result; } +constexpr std::array SrgbToLinear{ + 0x00000000u, 0x399f0000u, 0x3a1f0000u, 0x3a6f0000u, 0x3a9f0000u, 0x3ac70000u, 0x3aef0000u, 0x3b0b0000u, + 0x3b1f0000u, 0x3b330000u, 0x3b470000u, 0x3b5b0000u, 0x3b710000u, 0x3b840000u, 0x3b900000u, 0x3b9d0000u, + 0x3baa0000u, 0x3bb80000u, 0x3bc60000u, 0x3bd50000u, 0x3be50000u, 0x3bf60000u, 0x3c030000u, 0x3c0c0000u, + 0x3c160000u, 0x3c1f0000u, 0x3c290000u, 0x3c340000u, 0x3c3e0000u, 0x3c490000u, 0x3c550000u, 0x3c600000u, + 0x3c6d0000u, 0x3c790000u, 0x3c830000u, 0x3c8a0000u, 0x3c910000u, 0x3c980000u, 0x3c9f0000u, 0x3ca60000u, + 0x3cae0000u, 0x3cb60000u, 0x3cbe0000u, 0x3cc60000u, 0x3cce0000u, 0x3cd70000u, 0x3ce00000u, 0x3ce90000u, + 0x3cf20000u, 0x3cfc0000u, 0x3d030000u, 0x3d080000u, 0x3d0d0000u, 0x3d120000u, 0x3d170000u, 0x3d1c0000u, + 0x3d220000u, 0x3d280000u, 0x3d2d0000u, 0x3d330000u, 0x3d390000u, 0x3d3f0000u, 0x3d450000u, 0x3d4c0000u, + 0x3d520000u, 0x3d590000u, 0x3d5f0000u, 0x3d660000u, 0x3d6d0000u, 0x3d740000u, 0x3d7b0000u, 0x3d810000u, + 0x3d850000u, 0x3d880000u, 0x3d8c0000u, 0x3d900000u, 0x3d940000u, 0x3d980000u, 0x3d9c0000u, 0x3da00000u, + 0x3da40000u, 0x3da90000u, 0x3dad0000u, 0x3db10000u, 0x3db60000u, 0x3dba0000u, 0x3dbf0000u, 0x3dc30000u, + 0x3dc80000u, 0x3dcd0000u, 0x3dd10000u, 0x3dd60000u, 0x3ddb0000u, 0x3de00000u, 0x3de50000u, 0x3dea0000u, + 0x3df00000u, 0x3df50000u, 0x3dfa0000u, 0x3e000000u, 0x3e020000u, 0x3e050000u, 0x3e080000u, 0x3e0b0000u, + 0x3e0e0000u, 0x3e110000u, 0x3e140000u, 0x3e170000u, 0x3e1a0000u, 0x3e1d0000u, 0x3e200000u, 0x3e230000u, + 0x3e260000u, 0x3e290000u, 0x3e2c0000u, 0x3e300000u, 0x3e330000u, 0x3e360000u, 0x3e3a0000u, 0x3e3d0000u, + 0x3e400000u, 0x3e440000u, 0x3e470000u, 0x3e4b0000u, 0x3e4e0000u, 0x3e520000u, 0x3e560000u, 0x3e590000u, + 0x3e5d0000u, 0x3e610000u, 0x3e650000u, 0x3e680000u, 0x3e6c0000u, 0x3e700000u, 0x3e740000u, 0x3e780000u, + 0x3e7c0000u, 0x3e800000u, 0x3e820000u, 0x3e840000u, 0x3e860000u, 0x3e880000u, 0x3e8a0000u, 0x3e8d0000u, + 0x3e8f0000u, 0x3e910000u, 0x3e930000u, 0x3e950000u, 0x3e980000u, 0x3e9a0000u, 0x3e9c0000u, 0x3e9e0000u, + 0x3ea10000u, 0x3ea30000u, 0x3ea50000u, 0x3ea80000u, 0x3eaa0000u, 0x3ead0000u, 0x3eaf0000u, 0x3eb20000u, + 0x3eb40000u, 0x3eb60000u, 0x3eb90000u, 0x3ebc0000u, 0x3ebe0000u, 0x3ec10000u, 0x3ec30000u, 0x3ec60000u, + 0x3ec80000u, 0x3ecb0000u, 0x3ece0000u, 0x3ed10000u, 0x3ed30000u, 0x3ed60000u, 0x3ed90000u, 0x3edb0000u, + 0x3ede0000u, 0x3ee10000u, 0x3ee40000u, 0x3ee70000u, 0x3eea0000u, 0x3eed0000u, 0x3ef00000u, 0x3ef20000u, + 0x3ef50000u, 0x3ef80000u, 0x3efb0000u, 0x3efe0000u, 0x3f010000u, 0x3f020000u, 0x3f040000u, 0x3f050000u, + 0x3f070000u, 0x3f090000u, 0x3f0a0000u, 0x3f0c0000u, 0x3f0d0000u, 0x3f0f0000u, 0x3f110000u, 0x3f120000u, + 0x3f140000u, 0x3f160000u, 0x3f170000u, 0x3f190000u, 0x3f1b0000u, 0x3f1c0000u, 0x3f1e0000u, 0x3f200000u, + 0x3f210000u, 0x3f230000u, 0x3f250000u, 0x3f270000u, 0x3f290000u, 0x3f2a0000u, 0x3f2c0000u, 0x3f2e0000u, + 0x3f300000u, 0x3f320000u, 0x3f330000u, 0x3f350000u, 0x3f370000u, 0x3f390000u, 0x3f3b0000u, 0x3f3d0000u, + 0x3f3f0000u, 0x3f410000u, 0x3f430000u, 0x3f450000u, 0x3f470000u, 0x3f490000u, 0x3f4b0000u, 0x3f4d0000u, + 0x3f4f0000u, 0x3f510000u, 0x3f530000u, 0x3f550000u, 0x3f570000u, 0x3f590000u, 0x3f5b0000u, 0x3f5d0000u, + 0x3f5f0000u, 0x3f610000u, 0x3f630000u, 0x3f650000u, 0x3f680000u, 0x3f6a0000u, 0x3f6c0000u, 0x3f6e0000u, + 0x3f700000u, 0x3f730000u, 0x3f750000u, 0x3f770000u, 0x3f790000u, 0x3f7b0000u, 0x3f7e0000u, 0x3f800000u, +}; + +std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { + const auto pointerType = TypePointer(state, spv::StorageClassPrivate, TypeU32(state)); + std::array words{}; + for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, SrgbToLinear[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(words.size()))); + if (state.srgbTableVariable == 0) state.srgbTableVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, state.srgbTableVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); + std::uint32_t components[4] = {}; + for (std::uint32_t index = 0; index < 4u; index++) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), value, color, index); + const auto scaled = Binary(state, spv::OpFAdd, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), value, ConstantF32Value(state, 255.0f)), ConstantF32Value(state, 0.5f)); + const auto code = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), code, GlslStd450(state), GLSLstd450UMin, Unary(state, spv::OpConvertFToU, TypeU32(state), scaled), ConstantU32(state, 255u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, state.srgbTableVariable, code); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), bits, pointer); + components[index] = Unary(state, spv::OpBitcast, TypeF32(state), bits); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result, components[0], components[1], components[2], components[3]); + return result; +} + std::uint32_t QueryDimensions(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const auto dimension = access.image.dimension; @@ -437,6 +497,9 @@ void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAcce std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t texel) { auto& state = ctx.state; + if (access.image.srgbDecode) { + return DecodeSrgbTexel(state, texel); + } const auto info = ImageConversionFormat(access.image); if (info.format == IrBufferFormat::Invalid) { return texel; @@ -1192,6 +1255,9 @@ TableSelection TableSlot(SpirvValueEmitContext& ctx, const IrValue& inst, const } void EmitSamplingOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { + if (access.image.srgbDecode) { + ctx.Fail(access.inst, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); + } const auto setup = MakeSampleSetup(ctx, access); if (access.inst.Opcode() == IrOpcode::ImageGatherRaw) { EmitGatherOp(ctx, access, setup); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f5b3032d5..550f6320c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -194,6 +194,10 @@ Throughout the project, every function at every stage either **does exactly what - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. - [`ds_wrxchg2_rtn_b32` and `ds_wrxchg2st64_rtn_b32`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [64-bit image atomics](../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. +- [8_SRGB and 8_8_SRGB loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. +- [8_SRGB and 8_8_SRGB textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [Samples and gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. +- [Bindless image table](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). From 6e34e8614e7afd995df8c83b574565ae6f20125f Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:02:17 +0200 Subject: [PATCH 184/203] feat(agc): implement sint storage images through uint views Storage images of a SINT format threw "storage image descriptor uses an unsupported format"; only write-only 32_SINT was accepted. The recompiler now writes 16_SINT, 8_8_SINT, 32_SINT, 16_16_SINT, 8_8_8_8_SINT, 32_32_SINT, 16_16_16_16_SINT and 32_32_32_32_SINT storage images with the Uint numeric class. 8- and 16-bit components are saturated (EmitFormatStoreComponent) and 32-bit ones stored unchanged, as measured on RDNA2 (gfx1035); image_store_pck stores 32-bit components unchanged too. The driver binds these surfaces through a cached view of the same-size UINT format. 8_SINT, 32_32_32_SINT, the packed SINT formats (11_11_10, 10_11_11, 2_10_10_10, 10_10_10_2), D16 stores, packed stores of 8- and 16-bit components and atomics other than 32-bit ones on 32_SINT and 64-bit ones on 32_32_SINT still throw. New test: agc_driver_image_store_sint, with DST_SEL X,Y,0,1 on the two-channel formats, as with X,Y,Z,W the hardware writes channel 1 from the W data slot (TechnicalDebt). --- .../Graphics/include/Texture.hpp | 2 + .../Graphics/src/ShaderResources.cpp | 2 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 28 ++ .../tests/execution/ImageStoreSint.cpp | 316 ++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 7 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 21 +- docs/dev/TechnicalDebt.md | 2 + 7 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index ee2c5042d..44f685783 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -124,6 +124,7 @@ public: // so successive mip writes of a chain share one image and one write-back. VkImageView View(std::uint32_t mip); VkImageView FirstLayerView(std::uint32_t mip); + VkImageView StorageView(std::uint32_t mip, bool firstLayer); VkImageView AtomicView(std::uint32_t mip, bool firstLayer); VkImageView Atomic64View(std::uint32_t mip, bool firstLayer); // Render targets live in the same images: draws attach mip 0 through a view of the color @@ -455,6 +456,7 @@ private: std::map extraViews; std::map firstLayerViews; std::map, VkImageView> atomicViews; + std::map, VkImageView> uintViews; bool attachable = false; std::map, VkImageView> attachmentViews; VkFormat storageFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 86ff4ab3e..9a3ebd655 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1068,7 +1068,7 @@ void ShaderResources::buildComplete() { break; case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: write.pImageInfo = images.data() + images.size(); - for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageFirstLayer[index] ? storageTextures[index]->FirstLayerView(storageMips[index]) : storageTextures[index]->View(storageMips[index]), VK_IMAGE_LAYOUT_GENERAL}); + for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); break; case VK_DESCRIPTOR_TYPE_SAMPLER: write.pImageInfo = images.data() + images.size(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7e30db5e6..797e6f620 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -587,6 +587,20 @@ VkFormat StorageFormatOrUndefined(const Context& context, VkFormat format) { return storage; } +VkFormat UintFormatOfSint(VkFormat format) { + switch (format) { + case VK_FORMAT_R8G8_SINT: return VK_FORMAT_R8G8_UINT; + case VK_FORMAT_R8G8B8A8_SINT: return VK_FORMAT_R8G8B8A8_UINT; + case VK_FORMAT_R16_SINT: return VK_FORMAT_R16_UINT; + case VK_FORMAT_R16G16_SINT: return VK_FORMAT_R16G16_UINT; + case VK_FORMAT_R16G16B16A16_SINT: return VK_FORMAT_R16G16B16A16_UINT; + case VK_FORMAT_R32_SINT: return VK_FORMAT_R32_UINT; + case VK_FORMAT_R32G32_SINT: return VK_FORMAT_R32G32_UINT; + case VK_FORMAT_R32G32B32A32_SINT: return VK_FORMAT_R32G32B32A32_UINT; + default: return VK_FORMAT_UNDEFINED; + } +} + VkFormat StorageFormatFor(const Context& context, VkFormat format) { const auto storage = StorageFormatOrUndefined(context, format); if (storage == VK_FORMAT_UNDEFINED) Require(false, "guest storage texture format " + std::to_string(format) + " cannot be used as a storage image"); @@ -1008,6 +1022,18 @@ VkImageView StorageTexture::FirstLayerView(std::uint32_t mip) { return created; } +VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer) { + const auto format = UintFormatOfSint(storageFormat); + if (format == VK_FORMAT_UNDEFINED) return firstLayer ? FirstLayerView(mip) : View(mip); + if (format == VK_FORMAT_R32_UINT) return AtomicView(mip, firstLayer); + const auto found = uintViews.find({mip, firstLayer}); + if (found != uintViews.end()) return found->second; + Require(StorageFormatOrUndefined(context, format) == format, "storage image of format " + std::to_string(storageFormat) + " has no storage view of its UINT format " + std::to_string(format)); + const auto created = createView(mip, firstLayer, format); + uintViews.emplace(std::pair{mip, firstLayer}, created); + return created; +} + VkImageView StorageTexture::AtomicView(std::uint32_t mip, bool firstLayer) { if (storageFormat == VK_FORMAT_R32_UINT) return firstLayer ? FirstLayerView(mip) : View(mip); Require(storageFormat == VK_FORMAT_R32_SINT || storageFormat == VK_FORMAT_R32_SFLOAT, "storage image atomics need a surface of one 32-bit component"); @@ -3592,6 +3618,8 @@ void StorageTexture::release() noexcept { firstLayerViews.clear(); for (const auto& [key, atomic] : atomicViews) context.Function("vkDestroyImageView")(context.device, atomic, nullptr); atomicViews.clear(); + for (const auto& [key, uint] : uintViews) context.Function("vkDestroyImageView")(context.device, uint, nullptr); + uintViews.clear(); for (const auto& [format, attachment] : attachmentViews) context.Function("vkDestroyImageView")(context.device, attachment, nullptr); attachmentViews.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp new file mode 100644 index 000000000..5fa74af2e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp @@ -0,0 +1,316 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::uint32_t TitleSwizzle = 0x24cu; +constexpr std::size_t TexelBytes = 8192; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +alignas(256) constexpr std::array TitleCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0202108, 0x0001141e, + 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020084, 0x7e3c0300, 0x7e3e0280, 0xf0001f08, 0x0001141e, 0xbf8c3f70, 0xe0781000, 0x80001401, + 0xbf810000, +}; + +alignas(256) constexpr std::array D16Code{0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array PackedCode{0x7e3c0300, 0x7e3e0280, 0xf0281108, 0x0001141e, 0xbf810000}; +alignas(256) constexpr std::array AtomicD16Code{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array PackedWideCode{0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0281308, 0x0001141e, 0xbf810000}; +alignas(256) constexpr std::array AtomicCode{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xbf810000}; + +constexpr std::array Edges{ + 0x00000000u, 0x00000001u, 0xffffffffu, 0x0000007fu, 0x00000080u, 0xffffff80u, 0xffffff7fu, 0x000000ffu, + 0x00000100u, 0x00007fffu, 0x00008000u, 0xffff8000u, 0xffff7fffu, 0x0000ffffu, 0x00010000u, 0xffff0000u, + 0x7fffffffu, 0x80000000u, 0x12345678u, 0xedcba988u, 0x00007ffeu, 0xffff8001u, 0x0000007eu, 0xffffff81u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store sint: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t components; + std::uint32_t bits; +}; + +constexpr std::array Formats{{ + {"16_SINT", 12u, 1u, 16u}, + {"8_8_SINT", 19u, 2u, 8u}, + {"32_SINT", 21u, 1u, 32u}, + {"16_16_SINT", 28u, 2u, 16u}, + {"8_8_8_8_SINT", 61u, 4u, 8u}, + {"32_32_SINT", 63u, 2u, 32u}, + {"16_16_16_16_SINT", 70u, 4u, 16u}, + {"32_32_32_32_SINT", 76u, 4u, 32u}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}}; +constexpr std::array TitleWrites{{{0u, 0x1u}}}; +constexpr std::array PackedWideWrites{{{0u, 0x3u}}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid, std::uint32_t index) { + if (tid < Edges.size()) return Edges[(tid + 7u * index) % Edges.size()]; + const auto mixed = (tid * 4u + index + 1u) * 0x9e3779b1u; + return static_cast(static_cast(mixed) >> ((tid * 4u + index) % 25u)); +} + +std::uint32_t Saturate(std::uint32_t value, std::uint32_t bits) { + if (bits == 32u) return value; + const auto high = (std::int64_t{1} << (bits - 1u)) - 1; + const auto clamped = std::clamp(static_cast(value), -high - 1, high); + return static_cast(clamped) & ((1u << bits) - 1u); +} + +std::uint32_t SignExtend(std::uint32_t value, std::uint32_t bits) { + if (bits == 32u) return value; + const auto shift = 32u - bits; + return static_cast(static_cast(value << shift) >> shift); +} + +std::uint32_t SwizzleOf(const Format& format) { + return format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle; +} + +std::uint32_t TexelBytesOf(const Format& format) { + return format.components * format.bits / 8u; +} + +AgcDriver::Graphics::TileMipLayout Mip(const Format& format) { + return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, Height, 1u).at(0); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = Mip(format); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * TexelBytesOf(format); +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) { + bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + } + return bytes; +} + +std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + return Saturate(Value(tid, index), format.bits); +} + +std::vector Expected(const Format& format, std::span writes) { + auto bytes = Initial(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (const auto& write : writes) { + auto* texel = bytes.data() + TexelOffset(format, tid, write.row); + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto value = StoredComponent(format, tid, write.dmask, component); + std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); + } + } + } + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, std::uint32_t swizzle, const std::array& buffer) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, format, swizzle); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, std::uint32_t swizzle) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = Value(tid, index); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + Dispatch(device, texels, code, format, swizzle, BufferDescriptor(Input.data(), static_cast(Input.size() * 4u))); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format, std::span writes, const std::string& what) { + const auto expected = Expected(format, writes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::uint32_t wanted = 0; + std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u); + std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u); + Require(actual == wanted, what + " " + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted)); + } + } + } +} + +void CheckLoad(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + Output.fill(0xdeadbeefu); + Dispatch(device, texels, LoadCode, format.format, SwizzleOf(format), BufferDescriptor(Output.data(), static_cast(Output.size() * 4u))); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto actual = Output[tid * 4u + component]; + const auto wanted = SignExtend(StoredComponent(format, tid, 0xfu, component), format.bits); + Require(actual == wanted, std::string("image_load after image_store of ") + format.name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted)); + } + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, const std::string& reason, const std::string& what) { + std::string refusal; + try { + Store(device, texels, code, format, IdentitySwizzle); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(reason) != std::string::npos, what + " was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Store(*device, texels, StoreCode, format.format, SwizzleOf(format)); + CheckTexels(texels, format, StoreWrites, "image_store"); + CheckLoad(*device, texels, format); + } + Store(*device, texels, TitleCode, Formats[0].format, TitleSwizzle); + CheckTexels(texels, Formats[0], TitleWrites, "image_store dmask:0x1 glc with DST_SEL X,1,1,1"); + RequireRefused(*device, texels, D16Code, 12u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 16_SINT"); + RequireRefused(*device, texels, D16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 32_SINT"); + RequireRefused(*device, texels, PackedCode, 12u, "storage image descriptor uses an unsupported format", "image_store_pck of 16_SINT"); + RequireRefused(*device, texels, AtomicCode, 12u, "atomic image descriptor uses an unsupported format 12", "image_atomic_add of 16_SINT"); + RequireRefused(*device, texels, StoreCode, 73u, "storage image descriptor uses an unsupported format", "image_store of 32_32_32_SINT"); + for (const auto index : {2u, 5u, 7u}) { + Store(*device, texels, PackedWideCode, Formats[index].format, SwizzleOf(Formats[index])); + CheckTexels(texels, Formats[index], PackedWideWrites, "image_store_pck dmask:0x3"); + } + RequireRefused(*device, texels, AtomicD16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of an atomically updated 32_SINT image"); + std::puts("image store sint tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 2c46cb0a7..c30e16ee0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -172,7 +172,9 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag if (storage || decoded.conversionFormat != IrBufferFormat::Invalid) { decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); } - const bool rawSintStorage = storage && format == IrBufferFormat::Format32SInt && base.written && !base.read && !base.atomic; + const bool wideSint = format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt; + const bool narrowSint = format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8SInt || format == IrBufferFormat::Format16_16SInt || format == IrBufferFormat::Format8_8_8_8SInt || format == IrBufferFormat::Format16_16_16_16SInt; + const bool rawSintStorage = storage && (wideSint || (narrowSint && !base.packed)) && base.written && !base.read && !base.atomic; decoded.numericClass = base.atomic ? IrTextureNumericClass::Uint : SampledTextureNumericClass(format); if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { decoded.depthBits = true; @@ -187,6 +189,9 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag if (rawSintStorage) { decoded.numericClass = IrTextureNumericClass::Uint; } + if ((rawSintStorage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { + decoded.conversionFormat = format; + } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { throw std::runtime_error("sampled image descriptor uses an unsupported format"); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index eb77daa6d..c9598c1af 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -619,9 +619,25 @@ std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& return result; } +SpirvBufferFormatInfo SintStorageFormat(const ImageResource& image) { + const auto format = image.conversionFormat; + if (image.resourceClass != ImageResourceClass::Storage || format == IrBufferFormat::Invalid || SampledTextureNumericClass(format) != IrTextureNumericClass::Sint) { + return {}; + } + const auto info = GetFormatInfo(format); + if (image.numericClass != IrTextureNumericClass::Uint || info.type != SpirvFormatComponentType::Sint || info.packedBitfield || info.componentCount == 0u || info.byteSize == 12u) { + throw std::runtime_error("storage image conversion format is not a SINT format written through a UINT view"); + } + return info; +} + std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t data, bool integer) { auto& state = ctx.state; const auto& mem = access.mem; + const auto sint = SintStorageFormat(access.image); + if (sint.format != IrBufferFormat::Invalid && mem.dataBits == 16u) { + ctx.Fail(access.inst, "stores 16-bit data to an image of a SINT format"); + } const auto swizzle = access.image.shaderSwizzle; std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); @@ -640,10 +656,13 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce } } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); + if (component < sint.componentCount) { + values[component] = EmitFormatStoreComponent(state, sint, component, values[component]); + } } const auto texel = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, integer ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4), texel, values[0], values[1], values[2], values[3]); - return PackImageTexel(ctx, access, texel); + return sint.format != IrBufferFormat::Invalid ? texel : PackImageTexel(ctx, access, texel); } std::uint32_t PackedStoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t data) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6dc3ce18d..8ddfdd389 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -198,6 +198,8 @@ Throughout the project, every function at every stage either **does exactly what - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. +- [SINT storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. +- [`StoreTexel`](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. From 4fcfd3c2b3f5a7954f875324c086d0c0a5b18c1f Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Wed, 7 Oct 2026 15:42:04 +0200 Subject: [PATCH 185/203] feat(prx): stub TextToSpeech2, Vdecsw and Coredump unknown exports --- core/libs/prx/libSceTextToSpeech2/Export.cpp | 13 ++++ core/libs/prx/libSceVdecsw/Export.cpp | 73 +++++++++++++++++++ .../prx/libkernel/System/src/Coredump.cpp | 10 +++ docs/dev/TechnicalDebt.md | 14 ++++ 4 files changed, 110 insertions(+) create mode 100644 core/libs/prx/libSceTextToSpeech2/Export.cpp create mode 100644 core/libs/prx/libSceVdecsw/Export.cpp diff --git a/core/libs/prx/libSceTextToSpeech2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp new file mode 100644 index 000000000..2b94ca2f8 --- /dev/null +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceTextToSpeech2GetSystemStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp new file mode 100644 index 000000000..a0391dc7c --- /dev/null +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -0,0 +1,73 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVdecswCreateDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswDeleteDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswQueryComputeMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswFinalizeDecodeSequence() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswQueryDecoderMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswSetDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswReleaseComputeQueue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswAllocateComputeQueue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswGetAvcPictureInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswTrySyncDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswTrySyncDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswSetDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswResetDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 27efa3e65..84b33fec8 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -61,4 +61,14 @@ int APS5_VABI sceCoredumpGetStopInfoCpu(void) { NotImplemented_nid_no_patch("kK0DUW1Ukgc"); return 0; } + +int APS5_VABI sceCoredumpAttachUserMemoryFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpAttachMemoryRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 909a40108..fbfddc202 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -168,6 +168,20 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature +- [sceVdecswQueryComputeMemoryInfo](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswFinalizeDecodeSequence](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswQueryDecoderMemoryInfo](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswSetDecodeInput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswReleaseComputeQueue](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswAllocateComputeQueue](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswGetAvcPictureInfo](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswTrySyncDecodeOutput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswTrySyncDecodeInput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswSetDecodeOutput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswResetDecoder](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature +- [sceCoredumpAttachMemoryRegion](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. From 0e01d95abdf5a875861573e06f173468cafd62df Mon Sep 17 00:00:00 2001 From: RomainROCH Date: Tue, 6 Oct 2026 23:47:26 +0200 Subject: [PATCH 186/203] fix(libc): narrow nothrow allocation failure handling --- core/libs/prx/libc/src/StdioExtras.cpp | 17 +++++++++++++---- core/libs/tests/ApplicationHeap.cpp | 8 +++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index a29b690bc..09e1b5799 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -76,12 +77,20 @@ void* APS5_VABI _Znam_nid_postfix(std::size_t size) { return Allocate(size); } -void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) { - return Allocate(size); +void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { + try { + return Allocate(size); + } catch (const std::bad_alloc&) { + return nullptr; + } } -void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) { - return Allocate(size); +void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { + try { + return Allocate(size); + } catch (const std::bad_alloc&) { + return nullptr; + } } void APS5_VABI _ZdlPv_nid_postfix(void* pointer) { diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 87d87d5a5..8d882fdb9 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -12,7 +12,8 @@ extern "C" { int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); -void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*); +void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; +void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void APS5_VABI _ZdlPv_nid_postfix(void*); void APS5_VABI _ZdaPv_nid_postfix(void*); void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void*, std::size_t); @@ -201,6 +202,8 @@ int main(int argc, char** argv) { _ZdlPvSt11align_val_t_nid_postfix(nullptr, 64); require(frees == 5); fail = true; + require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); + require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); reject([] { ApplicationHeapAlign_nid_no_patch(4, 64); }); reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); @@ -233,6 +236,9 @@ int main(int argc, char** argv) { require(posix_memalign_nid_postfix(nullptr, 64, 32) == 22 && posixCalls == beforeInvalid); require(posix_memalign_nid_postfix(&unchanged, 64, 31) == 0 && unchanged == storage.data()); require(lastSize == 31 && lastAlignment == 64 && *__error_nid_postfix() == 77); + require(_ZnwmRKSt9nothrow_t_nid_postfix(16, nullptr) == storage.data() && lastSize == 16); + require(_ZnwmRKSt9nothrow_t_nid_postfix(0, nullptr) == storage.data() && lastSize == 1); + require(_ZnamRKSt9nothrow_t_nid_postfix(0, nullptr) == storage.data() && lastSize == 1); recurse = true; reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); recurse = false; From 14dc9b31ab633bfe62a6392c09bac91eb7232aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 3 Oct 2026 07:57:08 +0200 Subject: [PATCH 187/203] test(relinker): add C++ end-to-end fixture and --to-intel Linux stub test Add header-only support for end-to-end relinker tests: a minimal PS5 executable builder, a PT_LOAD mapper that applies segment permissions, and a posix_spawn runner for the relinker binary. Every core/relinker/tests/e2e/*.cpp becomes its own test, so new tests do not touch CMake. The first test relinks an executable with EXTRQ and INSERTQ sites using --to-intel, maps the output and calls the patched code, which runs both stubs from their Linux segment and returns through their branches. Co-Authored-By: Claude --- core/relinker/CMakeLists.txt | 10 ++ core/relinker/tests/e2e/ToIntelLinuxStubs.cpp | 64 +++++++++ core/relinker/tests/support/ElfFixture.hpp | 129 ++++++++++++++++++ .../tests/support/RelinkerProcess.hpp | 84 ++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 core/relinker/tests/e2e/ToIntelLinuxStubs.cpp create mode 100644 core/relinker/tests/support/ElfFixture.hpp create mode 100644 core/relinker/tests/support/RelinkerProcess.hpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 0df884a49..1a5b1fd38 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -274,4 +274,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include ) add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests) + + if(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + file(GLOB relinker_e2e_sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/e2e/*.cpp) + foreach(source ${relinker_e2e_sources}) + get_filename_component(name ${source} NAME_WE) + add_test_executable(relinker_e2e_${name} ${source}) + target_include_directories(relinker_e2e_${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/support) + add_test(NAME relinker_e2e_${name} COMMAND relinker_e2e_${name} $) + endforeach() + endif() endif() diff --git a/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp b/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp new file mode 100644 index 000000000..a9fceb1e4 --- /dev/null +++ b/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp @@ -0,0 +1,64 @@ +#include "ElfFixture.hpp" +#include "RelinkerProcess.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace RelinkerTests; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +const Bytes kCode = { + 0x66, 0x48, 0x0F, 0x6E, 0xCF, + 0x66, 0x48, 0x0F, 0x6E, 0xD6, + 0x66, 0x0F, 0x78, 0xC1, 0x08, 0x08, + 0xF2, 0x0F, 0x78, 0xD1, 0x08, 0x10, + 0x66, 0x48, 0x0F, 0x7E, 0xD0, + 0xC3, +}; +constexpr std::uint64_t kSites[] = {10, 16}; +constexpr std::size_t kSiteLength = 6; + +std::uint64_t expected(const std::uint64_t value, const std::uint64_t destination) { + return (destination & ~0xFF0000ull) | (((value >> 8) & 0xFF) << 16); +} + +} + +int main(const int argc, char** argv) { + try { + require(argc == 2, "usage: ToIntelLinuxStubs "); + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(kCode)); + const auto run = RunRelinker(argv[1], {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode == 0, "Relinker failed:\n" + run.Output); + require(run.Output.find("Intel conversion: 0 in place, 2 stubs") != std::string::npos, "Relinker did not report two stubs:\n" + run.Output); + + const MappedImage image(ReadFile(output)); + for (const auto site : kSites) { + const auto* bytes = image.At(site); + require(bytes[0] == 0xE9, "SSE4a site was not replaced by a jump"); + for (std::size_t index = 5; index < kSiteLength; ++index) require(bytes[index] == 0x90, "SSE4a site tail is not padded with NOPs"); + std::int32_t displacement; + std::memcpy(&displacement, bytes + 1, sizeof(displacement)); + require(site + 5 + displacement >= kCodeSize, "SSE4a stub is inside the original code segment"); + } + + const auto function = image.At(0); + for (const auto [value, destination] : {std::pair{0x1122334455667788ull, 0x0123456789ABCDEFull}, {0ull, ~0ull}, {~0ull, 0ull}, {0x9E3779B97F4A7C15ull, 0x0F1E2D3C4B5A6978ull}}) + require(function(value, destination) == expected(value, destination), "Relinked SSE4a stubs computed the wrong value"); + std::cout << "--to-intel Linux stub tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/tests/support/ElfFixture.hpp b/core/relinker/tests/support/ElfFixture.hpp new file mode 100644 index 000000000..9962b9d85 --- /dev/null +++ b/core/relinker/tests/support/ElfFixture.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RelinkerTests { + +using Bytes = std::vector; + +inline constexpr std::uint64_t kCodeOffset = 0x4000; +inline constexpr std::uint64_t kCodeSize = 0x1000; +inline constexpr std::uint64_t kDynamicVaddr = 0x600; +inline constexpr std::uint16_t kSpareProgramHeaders = 3; + +template +void Write(Bytes& bytes, const std::size_t offset, const TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF fixture write is out of bounds"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +template +TValue Read(const Bytes& bytes, const std::size_t offset) { + TValue value; + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF read is out of bounds"); + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +inline void WriteProgramHeader(Bytes& image, const std::uint16_t index, const std::uint32_t type, const std::uint32_t flags, const std::uint64_t offset, const std::uint64_t vaddr, const std::uint64_t size, const std::uint64_t align) { + const std::size_t header = 64 + index * 56; + Write(image, header, type); + Write(image, header + 4, flags); + Write(image, header + 8, offset); + Write(image, header + 16, vaddr); + Write(image, header + 24, vaddr); + Write(image, header + 32, size); + Write(image, header + 40, size); + Write(image, header + 48, align); +} + +inline Bytes MakeExecutable(const std::span code) { + if (code.size() > kDynamicVaddr) throw std::runtime_error("ELF fixture code overlaps the dynamic section"); + Bytes image(0x8000); + const std::uint8_t ident[16] = {0x7F, 'E', 'L', 'F', 2, 1, 1}; + std::memcpy(image.data(), ident, sizeof(ident)); + Write(image, 16, 3); + Write(image, 18, 62); + Write(image, 20, 1); + Write(image, 24, 0); + Write(image, 32, 64); + Write(image, 52, 64); + Write(image, 54, 56); + Write(image, 56, 2 + kSpareProgramHeaders); + Write(image, 58, 64); + const std::pair tags[] = {{5, 0x600}, {10, 1}, {6, 0x620}, {11, 24}, {7, 0x700}, {8, 0}, {9, 24}, {0, 0}}; + WriteProgramHeader(image, 0, 1, 5, kCodeOffset, 0, kCodeSize, 0x4000); + WriteProgramHeader(image, 1, 2, 6, kCodeOffset + kDynamicVaddr, kDynamicVaddr, sizeof(tags) / sizeof(tags[0]) * 16, 8); + for (std::uint16_t index = 2; index < 2 + kSpareProgramHeaders; ++index) WriteProgramHeader(image, index, 0x6FFFFF01, 0, 0, 0, 0, 1); + std::fill(image.begin() + kCodeOffset, image.begin() + kCodeOffset + kDynamicVaddr, 0xCC); + std::copy(code.begin(), code.end(), image.begin() + kCodeOffset); + for (std::size_t index = 0; index < sizeof(tags) / sizeof(tags[0]); ++index) { + Write(image, kCodeOffset + kDynamicVaddr + index * 16, tags[index].first); + Write(image, kCodeOffset + kDynamicVaddr + index * 16 + 8, tags[index].second); + } + return image; +} + +class MappedImage { +public: + explicit MappedImage(const Bytes& elf) { + const auto phOffset = Read(elf, 32); + const auto phSize = Read(elf, 54); + const auto phCount = Read(elf, 56); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + size = std::max(size, Read(elf, header + 16) + Read(elf, header + 40)); + } + if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image"); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + const auto offset = Read(elf, header + 8); + const auto fileSize = Read(elf, header + 32); + if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file"); + std::memcpy(static_cast(base) + Read(elf, header + 16), elf.data() + offset, fileSize); + } + if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image"); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + const auto flags = Read(elf, header + 4); + const auto vaddr = Read(elf, header + 16); + const auto start = vaddr / page * page; + const auto end = (vaddr + Read(elf, header + 40) + page - 1) / page * page; + const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0); + if (mprotect(static_cast(base) + start, std::min(end, size) - start, protection) != 0) throw std::runtime_error("Cannot apply PT_LOAD permissions"); + } + } + + MappedImage(const MappedImage&) = delete; + MappedImage& operator=(const MappedImage&) = delete; + + ~MappedImage() { + munmap(base, size); + } + + template + TFunction* At(const std::uint64_t vaddr) const { + if (vaddr >= size) throw std::runtime_error("Address is outside the mapped ELF image"); + return reinterpret_cast(static_cast(base) + vaddr); + } + +private: + void* base = nullptr; + std::size_t size = 0; +}; + +} // namespace RelinkerTests diff --git a/core/relinker/tests/support/RelinkerProcess.hpp b/core/relinker/tests/support/RelinkerProcess.hpp new file mode 100644 index 000000000..b642783fe --- /dev/null +++ b/core/relinker/tests/support/RelinkerProcess.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace RelinkerTests { + +class TempDirectory { +public: + TempDirectory() { + auto pattern = (std::filesystem::temp_directory_path() / "anyps5-relinker-XXXXXX").string(); + if (mkdtemp(pattern.data()) == nullptr) throw std::runtime_error("Cannot create a temporary directory"); + path = pattern; + } + + TempDirectory(const TempDirectory&) = delete; + TempDirectory& operator=(const TempDirectory&) = delete; + + ~TempDirectory() { + std::error_code error; + std::filesystem::remove_all(path, error); + } + + const std::filesystem::path& Path() const { + return path; + } + +private: + std::filesystem::path path; +}; + +struct RelinkerRun { + int ExitCode = -1; + std::string Output; +}; + +inline RelinkerRun RunRelinker(const std::filesystem::path& relinker, const std::vector& arguments, const std::filesystem::path& logPath) { + std::vector owned{relinker.string()}; + owned.insert(owned.end(), arguments.begin(), arguments.end()); + std::vector argv; + for (auto& argument : owned) argv.push_back(argument.data()); + argv.push_back(nullptr); + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, logPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO); + pid_t pid = 0; + const auto spawned = posix_spawn(&pid, argv[0], &actions, nullptr, argv.data(), environ); + posix_spawn_file_actions_destroy(&actions); + if (spawned != 0) throw std::runtime_error("Cannot start the relinker"); + int status = 0; + if (waitpid(pid, &status, 0) != pid) throw std::runtime_error("Cannot wait for the relinker"); + RelinkerRun run; + if (WIFEXITED(status)) run.ExitCode = WEXITSTATUS(status); + std::ifstream log(logPath, std::ios::binary); + run.Output.assign(std::istreambuf_iterator(log), std::istreambuf_iterator()); + return run; +} + +inline std::vector ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("Cannot read " + path.string()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +inline void WriteFile(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file) throw std::runtime_error("Cannot write " + path.string()); +} + +} // namespace RelinkerTests From 9f3ac9a5bd3fb764bd2740a53bc567046a79b54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 16:03:41 +0200 Subject: [PATCH 188/203] test(relinker): register the --to-intel stub test next to the relinker tests Co-Authored-By: Claude --- core/relinker/CMakeLists.txt | 11 ++++------- .../{tests/support => relinker/tests}/ElfFixture.hpp | 0 .../support => relinker/tests}/RelinkerProcess.hpp | 0 .../tests/ToIntelLinuxStubsTests.cpp} | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) rename core/relinker/{tests/support => relinker/tests}/ElfFixture.hpp (100%) rename core/relinker/{tests/support => relinker/tests}/RelinkerProcess.hpp (100%) rename core/relinker/{tests/e2e/ToIntelLinuxStubs.cpp => relinker/tests/ToIntelLinuxStubsTests.cpp} (97%) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 1a5b1fd38..fddfe88ff 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -276,12 +276,9 @@ if(BUILD_TESTING) add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests) if(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") - file(GLOB relinker_e2e_sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/e2e/*.cpp) - foreach(source ${relinker_e2e_sources}) - get_filename_component(name ${source} NAME_WE) - add_test_executable(relinker_e2e_${name} ${source}) - target_include_directories(relinker_e2e_${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/support) - add_test(NAME relinker_e2e_${name} COMMAND relinker_e2e_${name} $) - endforeach() + add_test_executable(to_intel_linux_stubs_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ToIntelLinuxStubsTests.cpp + ) + add_test(NAME to_intel_linux_stubs COMMAND to_intel_linux_stubs_tests $) endif() endif() diff --git a/core/relinker/tests/support/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp similarity index 100% rename from core/relinker/tests/support/ElfFixture.hpp rename to core/relinker/relinker/tests/ElfFixture.hpp diff --git a/core/relinker/tests/support/RelinkerProcess.hpp b/core/relinker/relinker/tests/RelinkerProcess.hpp similarity index 100% rename from core/relinker/tests/support/RelinkerProcess.hpp rename to core/relinker/relinker/tests/RelinkerProcess.hpp diff --git a/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp b/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp similarity index 97% rename from core/relinker/tests/e2e/ToIntelLinuxStubs.cpp rename to core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp index a9fceb1e4..04a57131f 100644 --- a/core/relinker/tests/e2e/ToIntelLinuxStubs.cpp +++ b/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp @@ -34,7 +34,7 @@ std::uint64_t expected(const std::uint64_t value, const std::uint64_t destinatio int main(const int argc, char** argv) { try { - require(argc == 2, "usage: ToIntelLinuxStubs "); + require(argc == 2, "usage: to_intel_linux_stubs_tests "); const TempDirectory directory; const auto input = directory.Path() / "input.elf"; const auto output = directory.Path() / "output.elf"; From 34026e88f9d19cb1f4754e7d1e034275b7011a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 3 Oct 2026 17:18:12 +0200 Subject: [PATCH 189/203] ci: check pull requests against the contributing rules Co-Authored-By: Claude --- .github/workflows/conventions.yml | 22 +++ CONTRIBUTING.md | 6 + tools/check_conventions.py | 274 ++++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 .github/workflows/conventions.yml create mode 100644 tools/check_conventions.py diff --git a/.github/workflows/conventions.yml b/.github/workflows/conventions.yml new file mode 100644 index 000000000..a6ef53eb0 --- /dev/null +++ b/.github/workflows/conventions.yml @@ -0,0 +1,22 @@ +name: Conventions + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + +concurrency: + group: conventions-${{ github.ref }} + cancel-in-progress: true + +jobs: + conventions: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - run: python3 tools/check_conventions.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 212bdb9a3..f36977c5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,12 @@ ctest --test-dir build --output-on-failure Python 3 is optional; without it some relinker tests are not registered. +The Conventions check runs on every pull request and fails when a rule on this page is broken. Run it locally before pushing: + +``` +python3 tools/check_conventions.py --base origin/main +``` + ## Branches and pull requests The [pull request template](.github/pull_request_template.md) is the checklist for these rules. diff --git a/tools/check_conventions.py b/tools/check_conventions.py new file mode 100644 index 000000000..3a0bce46e --- /dev/null +++ b/tools/check_conventions.py @@ -0,0 +1,274 @@ +import argparse +import json +import os +import re +import subprocess +from collections import defaultdict +from pathlib import PurePosixPath + +RULES = { + "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, the end of #endif and the end of a namespace"), + "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed in docs/dev/TechnicalDebt.md#silent-stubs"), + "duplicate-export": ("error", "CONTRIBUTING.md#code", "Already exported by another library: remove the copy, or use APS5_DUMMY_FUN if nothing is left"), + "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), + "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), + "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), + "host-path": ("error", "CONTRIBUTING.md#code", "Files stay next to the executable; nothing is read or written in user or system folders"), + "deprecated-flag": ("error", "docs/user/USAGE.md", "Deprecated relinker flag, only for debugging"), + "fallback": ("warning", "CONTRIBUTING.md#code", "Looks like a fallback; failures must stay explicit"), + "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), + "binary": ("error", "docs/dev/CONVENTIONS.md", "No images or binary files in the repository; images go in the gist comments"), + "size": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Too large to review; split it into several pull requests"), + "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), + "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), + "revert-commit": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Don't git revert inside a pull request; drop or rewrite the commit"), + "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), + "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), +} + +CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} +IMAGES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tga", ".dds"} +NOTES = {".md", ".markdown", ".rst", ".log", ".patch", ".diff"} +AGENT_FILES = {"AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursorrules", "copilot-instructions.md"} +MAX_LINES = 2000 + +CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") +STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') +ALLOWED_COMMENT = re.compile(r"^\s*(#\s*endif\b|}\s*;?\s*//\s*(end\s+)?namespace\b)") +EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") +STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s*;)*" + r"(?:return\s*\(?\s*(?:-?(?:0x[0-9a-fA-F]+|\d+)[uUlL]*|nullptr|true|false|\w*_OK)?\s*\)?\s*;)?$") +SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules|pkg_search_module|find_library)\b") +TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") +EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") +HOST_PATH = re.compile(r'getenv\s*\(\s*"(HOME|USERPROFILE|APPDATA|LOCALAPPDATA|TEMP|TMP|TMPDIR|XDG_\w+)"|temp_directory_path|SHGetKnownFolderPath|SHGetFolderPath') +DEPRECATED = re.compile(r"--(skip-sce-module|exclude-sce-module|skip-syscall-check|lazy-binding)\b") +FALLBACK = re.compile(r"(?i)fall[ _-]?back|retry|retries|best[ _-]?effort") +LINK = re.compile(r"\]\(([^)\s]+)") +SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) + + +def git(*args): + return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout + + +class Check: + def __init__(self, base, head): + self.head = head + self.base = git("merge-base", base, head).strip() + self.findings = [] + self.added = defaultdict(list) + self.status = {} + self.numstat = {} + self.tree = set(git("ls-tree", "-r", "--name-only", head).splitlines()) + self.dirs = {str(parent) for path in self.tree for parent in PurePosixPath(path).parents} + for line in git("diff", "--name-status", "--no-renames", self.base, head).splitlines(): + status, path = line.split("\t", 1) + self.status[path] = status + for line in git("diff", "--numstat", "--no-renames", self.base, head).splitlines(): + added, deleted, path = line.split("\t", 2) + self.numstat[path] = (added, deleted) + path = None + for line in git("diff", "-U0", "--no-color", "--no-renames", "--no-prefix", self.base, head).splitlines(): + if line.startswith("+++ "): + path = None if line == "+++ /dev/null" else line[4:] + elif line.startswith("@@"): + number = int(re.search(r"\+(\d+)", line).group(1)) + elif line.startswith("+") and path: + self.added[path].append((number, line[1:])) + number += 1 + + def report(self, rule, path, line, detail=""): + self.findings.append((rule, path, line, detail)) + + def files(self, predicate): + return [(path, lines) for path, lines in self.added.items() if not path.startswith("3rdparty/") and predicate(PurePosixPath(path))] + + def show(self, path): + return git("show", f"{self.head}:{path}") + + def code(self): + debt = self.show("docs/dev/TechnicalDebt.md") if "docs/dev/TechnicalDebt.md" in self.tree else "" + exports = {} + for path, lines in self.files(lambda p: p.suffix in CPP): + core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts + source = None + for number, text in lines: + bare = STRINGS.sub('""', text) + if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): + self.report("comment", path, number) + if EXTENSION.search(bare): + self.report("extension", path, number, EXTENSION.search(bare).group(0)) + if core and TITLE.search(text): + self.report("title-specific", path, number, TITLE.search(text).group(0)) + if core and HOST_PATH.search(text): + self.report("host-path", path, number, HOST_PATH.search(text).group(0)) + if core and FALLBACK.search(text): + self.report("fallback", path, number, FALLBACK.search(text).group(0)) + match = EXPORT.search(bare) + if match and path.startswith("core/libs/prx/") and not match.group(1).endswith("_nid_no_patch"): + source = source or self.show(path).splitlines() + body = function_body(source, number - 1) + if body is None: + continue + exports[match.group(1)] = (path, number) + if STUB_BODY.match(body) and match.group(1) not in debt: + self.report("silent-stub", path, number, match.group(1)) + self.duplicates(exports) + for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): + for number, text in lines: + if "#" in STRINGS.sub('""', text) and not text.startswith("#!"): + self.report("comment", path, number) + + def duplicates(self, exports): + if not exports: + return + pattern = r"\bAPS5_VABI\s+(" + "|".join(exports) + r")\s*\(" + result = subprocess.run(["git", "grep", "-nE", pattern, self.head, "--", "core/libs/prx"], capture_output=True, text=True) + for line in result.stdout.splitlines(): + _, path, _, text = line.split(":", 3) + name = EXPORT.search(text).group(1) + own, number = exports[name] + if library(path) != library(own) and not text.rstrip().endswith(";"): + self.report("duplicate-export", own, number, f"{name} is also in {library(path)}") + + def build(self): + for path, lines in self.files(lambda p: p.name == "CMakeLists.txt" or p.suffix == ".cmake"): + for number, text in lines: + if SYSTEM_DEPENDENCY.search(text.split("#", 1)[0]): + self.report("system-dependency", path, number, text.strip()) + for number, text in self.added.get(".gitmodules", []): + match = re.match(r"\s*path\s*=\s*(\S+)", text) + if match and not match.group(1).startswith("3rdparty/"): + self.report("system-dependency", ".gitmodules", number, match.group(1)) + for path, lines in self.files(lambda p: not p.parts[:2] == ("core", "relinker") and str(p) != "docs/user/USAGE.md"): + for number, text in lines: + if DEPRECATED.search(text): + self.report("deprecated-flag", path, number, DEPRECATED.search(text).group(0)) + + def repository(self): + total = 0 + for path, (added, deleted) in self.numstat.items(): + if path.startswith("3rdparty/"): + continue + suffix = PurePosixPath(path).suffix.lower() + if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES): + self.report("binary", path, 0) + if added != "-": + total += int(added) + int(deleted) + if self.status.get(path) == "A" and allowed_notes(path) is False: + self.report("notes-file", path, 0) + if total > MAX_LINES: + self.report("size", "", 0, f"{total} changed lines outside 3rdparty/, limit {MAX_LINES}") + + def docs(self): + for path, lines in self.files(lambda p: p.suffix == ".md"): + for number, text in lines: + for target in LINK.findall(text): + if re.match(r"[a-z]+:|#", target): + continue + target = target.split("#", 1)[0].split("?", 1)[0] + resolved = os.path.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)) + if resolved not in self.tree and resolved not in self.dirs: + self.report("doc-link", path, number, target) + + def commits(self): + for line in git("log", "--no-merges", "--format=%h %s", f"{self.base}..{self.head}").splitlines(): + sha, subject = line.split(" ", 1) + if subject.startswith('Revert "'): + self.report("revert-commit", "", 0, f"{sha} {subject}") + elif not CONVENTIONAL.match(subject): + self.report("commit-subject", "", 0, f"{sha} {subject}") + + def pull_request(self, title, body): + if not CONVENTIONAL.match(title): + self.report("pr-title", "", 0, title) + body = re.sub(r"", "", body or "", flags=re.S) + sections = dict(zip(SECTION.findall(body), SECTION.split(body)[2::2])) + for name in ("What", "Tested", "Checklist"): + if not sections.get(name, "").strip(): + self.report("pr-template", "", 0, f"section '{name}' is missing or empty") + checklist = sections.get("Checklist", "") + for item in re.findall(r"^\s*-\s*\[ \]\s*(.+)$", checklist, re.M): + self.report("pr-template", "", 0, f"unchecked: {item.strip()}") + if checklist and not re.search(r"AI-assisted:\s*(yes|no)\b", checklist, re.I): + self.report("pr-template", "", 0, "answer AI-assisted with yes or no") + if checklist and not re.search(r"Depends on:\s*(#\d+|none)\b", checklist, re.I): + self.report("pr-template", "", 0, "fill Depends on with #N or none") + + +def library(path): + return PurePosixPath(path).parts[3] + + +def allowed_notes(path): + p = PurePosixPath(path) + if p.name in AGENT_FILES: + return False + if p.suffix.lower() not in NOTES: + return None + return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) + + +def function_body(lines, start): + text = "\n".join(lines[start:start + 400]) + text = re.sub(r"//.*|/\*.*?\*/", "", STRINGS.sub('""', text), flags=re.S) + brace, semicolon = text.find("{"), text.find(";") + if brace < 0 or 0 <= semicolon < brace: + return None + depth = 0 + for index in range(brace, len(text)): + depth += {"{": 1, "}": -1}.get(text[index], 0) + if depth == 0: + return re.sub(r"\s+", " ", text[brace + 1:index]).replace(" ", "") + return None + + +def output(check, repo): + errors = 0 + rows = [] + actions = os.environ.get("GITHUB_ACTIONS") == "true" + for rule, path, line, detail in sorted(check.findings, key=lambda f: (not f[1], f[1], f[2], f[0])): + level, doc, message = RULES[rule] + errors += level == "error" + text = f"{message}" + (f": {detail}" if detail else "") + if actions: + location = f" file={path},line={max(line, 1)}," if path else " " + print(f"::{level}{location}title={rule}::" + text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")) + else: + print(f"{path or '(pull request)'}{f':{line}' if line else ''}: {level}: [{rule}] {text}") + where = f"`{path}:{line}`" if line else f"`{path}`" if path else "" + cell = text.replace("|", "\\|") + rows.append(f"| {level} | [{rule}](https://github.com/{repo}/blob/main/{doc}) | {where} | {cell} |") + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if summary: + with open(summary, "a") as out: + if rows: + out.write("| Level | Rule | Where | What |\n|---|---|---|---|\n" + "\n".join(rows) + "\n") + else: + out.write("All conventions checks passed.\n") + print(f"{len(rows)} findings, {errors} errors; GitHub shows at most 10 error annotations, the job summary lists all" + if rows else "All conventions checks passed.") + return 1 if errors else 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="check a branch against the contributing rules; errors fail the check") + parser.add_argument("--base", default="origin/main", help="branch the pull request targets (default: origin/main)") + parser.add_argument("--head", default="HEAD") + parser.add_argument("--title", help="pull request title to check") + parser.add_argument("--body", help="file with the pull request description to check") + parser.add_argument("--event", default=os.environ.get("GITHUB_EVENT_PATH"), help="GitHub event JSON (set in CI)") + args = parser.parse_args() + check = Check(args.base, args.head) + check.code() + check.build() + check.repository() + check.docs() + check.commits() + pr = json.load(open(args.event)).get("pull_request") if args.event else None + if pr: + check.pull_request(pr["title"], pr["body"]) + elif args.title is not None or args.body: + check.pull_request(args.title or "", open(args.body).read() if args.body else "") + raise SystemExit(output(check, os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5"))) From d3501453fdcd2a1e95551c3a9ea5469c491764a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 16:10:27 +0200 Subject: [PATCH 190/203] ci(conventions): keep only the rules written in CONTRIBUTING --- tools/check_conventions.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 3a0bce46e..2a2a96e1e 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -13,15 +13,10 @@ RULES = { "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), - "host-path": ("error", "CONTRIBUTING.md#code", "Files stay next to the executable; nothing is read or written in user or system folders"), - "deprecated-flag": ("error", "docs/user/USAGE.md", "Deprecated relinker flag, only for debugging"), - "fallback": ("warning", "CONTRIBUTING.md#code", "Looks like a fallback; failures must stay explicit"), "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), "binary": ("error", "docs/dev/CONVENTIONS.md", "No images or binary files in the repository; images go in the gist comments"), - "size": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Too large to review; split it into several pull requests"), "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), - "revert-commit": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Don't git revert inside a pull request; drop or rewrite the commit"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), } @@ -30,7 +25,6 @@ CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} IMAGES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tga", ".dds"} NOTES = {".md", ".markdown", ".rst", ".log", ".patch", ".diff"} AGENT_FILES = {"AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursorrules", "copilot-instructions.md"} -MAX_LINES = 2000 CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') @@ -41,9 +35,6 @@ STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules|pkg_search_module|find_library)\b") TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") -HOST_PATH = re.compile(r'getenv\s*\(\s*"(HOME|USERPROFILE|APPDATA|LOCALAPPDATA|TEMP|TMP|TMPDIR|XDG_\w+)"|temp_directory_path|SHGetKnownFolderPath|SHGetFolderPath') -DEPRECATED = re.compile(r"--(skip-sce-module|exclude-sce-module|skip-syscall-check|lazy-binding)\b") -FALLBACK = re.compile(r"(?i)fall[ _-]?back|retry|retries|best[ _-]?effort") LINK = re.compile(r"\]\(([^)\s]+)") SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) @@ -101,10 +92,6 @@ class Check: self.report("extension", path, number, EXTENSION.search(bare).group(0)) if core and TITLE.search(text): self.report("title-specific", path, number, TITLE.search(text).group(0)) - if core and HOST_PATH.search(text): - self.report("host-path", path, number, HOST_PATH.search(text).group(0)) - if core and FALLBACK.search(text): - self.report("fallback", path, number, FALLBACK.search(text).group(0)) match = EXPORT.search(bare) if match and path.startswith("core/libs/prx/") and not match.group(1).endswith("_nid_no_patch"): source = source or self.show(path).splitlines() @@ -112,7 +99,7 @@ class Check: if body is None: continue exports[match.group(1)] = (path, number) - if STUB_BODY.match(body) and match.group(1) not in debt: + if match.group(1).startswith("sce") and STUB_BODY.match(body) and match.group(1) not in debt: self.report("silent-stub", path, number, match.group(1)) self.duplicates(exports) for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): @@ -141,25 +128,16 @@ class Check: match = re.match(r"\s*path\s*=\s*(\S+)", text) if match and not match.group(1).startswith("3rdparty/"): self.report("system-dependency", ".gitmodules", number, match.group(1)) - for path, lines in self.files(lambda p: not p.parts[:2] == ("core", "relinker") and str(p) != "docs/user/USAGE.md"): - for number, text in lines: - if DEPRECATED.search(text): - self.report("deprecated-flag", path, number, DEPRECATED.search(text).group(0)) def repository(self): - total = 0 for path, (added, deleted) in self.numstat.items(): if path.startswith("3rdparty/"): continue suffix = PurePosixPath(path).suffix.lower() if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES): self.report("binary", path, 0) - if added != "-": - total += int(added) + int(deleted) if self.status.get(path) == "A" and allowed_notes(path) is False: self.report("notes-file", path, 0) - if total > MAX_LINES: - self.report("size", "", 0, f"{total} changed lines outside 3rdparty/, limit {MAX_LINES}") def docs(self): for path, lines in self.files(lambda p: p.suffix == ".md"): @@ -175,9 +153,7 @@ class Check: def commits(self): for line in git("log", "--no-merges", "--format=%h %s", f"{self.base}..{self.head}").splitlines(): sha, subject = line.split(" ", 1) - if subject.startswith('Revert "'): - self.report("revert-commit", "", 0, f"{sha} {subject}") - elif not CONVENTIONAL.match(subject): + if not CONVENTIONAL.match(subject): self.report("commit-subject", "", 0, f"{sha} {subject}") def pull_request(self, title, body): From 5cb47befe3ab64b156ab4d2aa8b570be87f0c550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 17:04:00 +0200 Subject: [PATCH 191/203] ci(conventions): allow comments with a TechnicalDebt change, reject non-text files --- CONTRIBUTING.md | 2 +- tools/check_conventions.py | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f36977c5e..a901ee126 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ ctest --test-dir build --output-on-failure Python 3 is optional; without it some relinker tests are not registered. -The Conventions check runs on every pull request and fails when a rule on this page is broken. Run it locally before pushing: +The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing: ``` python3 tools/check_conventions.py --base origin/main diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 2a2a96e1e..8d0de9a8d 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -7,14 +7,14 @@ from collections import defaultdict from pathlib import PurePosixPath RULES = { - "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, the end of #endif and the end of a namespace"), + "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md in the same pull request; #endif and namespace ends are always allowed"), "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed in docs/dev/TechnicalDebt.md#silent-stubs"), "duplicate-export": ("error", "CONTRIBUTING.md#code", "Already exported by another library: remove the copy, or use APS5_DUMMY_FUN if nothing is left"), "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), - "binary": ("error", "docs/dev/CONVENTIONS.md", "No images or binary files in the repository; images go in the gist comments"), + "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), @@ -39,8 +39,9 @@ LINK = re.compile(r"\]\(([^)\s]+)") SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) -def git(*args): - return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout +def git(*args, text=True): + output = subprocess.run(["git", *args], capture_output=True, check=True).stdout + return output.decode("utf-8", errors="replace") if text else output class Check: @@ -106,6 +107,8 @@ class Check: for number, text in lines: if "#" in STRINGS.sub('""', text) and not text.startswith("#!"): self.report("comment", path, number) + if "docs/dev/TechnicalDebt.md" in self.status: + self.findings = [finding for finding in self.findings if finding[0] != "comment"] def duplicates(self, exports): if not exports: @@ -134,7 +137,7 @@ class Check: if path.startswith("3rdparty/"): continue suffix = PurePosixPath(path).suffix.lower() - if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES): + if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(git("show", f"{self.head}:{path}", text=False))): self.report("binary", path, 0) if self.status.get(path) == "A" and allowed_notes(path) is False: self.report("notes-file", path, 0) @@ -173,6 +176,14 @@ class Check: self.report("pr-template", "", 0, "fill Depends on with #N or none") +def utf8(data): + try: + data.decode("utf-8") + except UnicodeDecodeError: + return False + return True + + def library(path): return PurePosixPath(path).parts[3] From bdf7cc4ef06fd03b44271004bc7da09a3561bd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 7 Oct 2026 17:24:27 +0200 Subject: [PATCH 192/203] ci(conventions): accept silent stubs listed by file or by name with a path in TechnicalDebt Co-Authored-By: Claude --- tools/check_conventions.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8d0de9a8d..933dec172 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -8,7 +8,7 @@ from pathlib import PurePosixPath RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md in the same pull request; #endif and namespace ends are always allowed"), - "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed in docs/dev/TechnicalDebt.md#silent-stubs"), + "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed in docs/dev/TechnicalDebt.md#silent-stubs, by their file or by name with a path in their library"), "duplicate-export": ("error", "CONTRIBUTING.md#code", "Already exported by another library: remove the copy, or use APS5_DUMMY_FUN if nothing is left"), "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), @@ -80,7 +80,7 @@ class Check: return git("show", f"{self.head}:{path}") def code(self): - debt = self.show("docs/dev/TechnicalDebt.md") if "docs/dev/TechnicalDebt.md" in self.tree else "" + debt = [(text, links("docs/dev/TechnicalDebt.md", text)) for text in self.show("docs/dev/TechnicalDebt.md").splitlines()] if "docs/dev/TechnicalDebt.md" in self.tree else [] exports = {} for path, lines in self.files(lambda p: p.suffix in CPP): core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts @@ -100,7 +100,7 @@ class Check: if body is None: continue exports[match.group(1)] = (path, number) - if match.group(1).startswith("sce") and STUB_BODY.match(body) and match.group(1) not in debt: + if match.group(1).startswith("sce") and STUB_BODY.match(body) and not listed(debt, match.group(1), path): self.report("silent-stub", path, number, match.group(1)) self.duplicates(exports) for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): @@ -145,11 +145,7 @@ class Check: def docs(self): for path, lines in self.files(lambda p: p.suffix == ".md"): for number, text in lines: - for target in LINK.findall(text): - if re.match(r"[a-z]+:|#", target): - continue - target = target.split("#", 1)[0].split("?", 1)[0] - resolved = os.path.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)) + for target, resolved in links(path, text): if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) @@ -184,6 +180,25 @@ def utf8(data): return True +def links(path, text): + result = [] + for target in LINK.findall(text): + if re.match(r"[a-z]+:|#", target): + continue + target = target.split("#", 1)[0].split("?", 1)[0] + result.append((target, os.path.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)))) + return result + + +def listed(debt, name, path): + own = PurePosixPath(path).parts[:4] + for text, targets in debt: + for _, resolved in targets: + if resolved == path or (PurePosixPath(resolved).parts[:4] == own and re.search(rf"\b{name}\b", text)): + return True + return False + + def library(path): return PurePosixPath(path).parts[3] From a620a2579d6b60172ea80587b368116920aff46f Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 4 Oct 2026 16:29:25 +0200 Subject: [PATCH 193/203] fix(libs): TLOU throwing stubs for VideoOut VrrPeg and VrrStatus --- core/libs/prx/libSceVideoOut/src/Output.cpp | 29 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 4 +++ 2 files changed, 33 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index b3af7d8ed..74b011e8a 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -276,4 +276,33 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } +APS5_EXPORT("T4ucGB8CsnM", sceVideoOutVrrPeg); +APS5_EXPORT("5tRaBjtdTzY", sceVideoOutVrrPeg); +int APS5_VABI sceVideoOutVrrPeg(std::uint64_t a0, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3) try { + (void)a0; + (void)a1; + (void)a2; + (void)a3; + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +APS5_EXPORT("kP2L8t3j-aM", sceVideoOutVrrStatusUnknown00); +int APS5_VABI sceVideoOutVrrStatusUnknown00() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +APS5_EXPORT("LibwuIonIBw", sceVideoOutVrrStatusUnknown01); +int APS5_VABI sceVideoOutVrrStatusUnknown01() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fbfddc202..e6c2dc8f2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -67,6 +67,10 @@ Throughout the project, every function at every stage either **does exactly what - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` - [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) +- [LibwuIonIBw](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, signature +- [T4ucGB8CsnM](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, takes four opaque 64-bit arguments +- [5tRaBjtdTzY](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, takes four opaque 64-bit arguments +- [kP2L8t3j-aM](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, takes no arguments - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From 0b3dd07ba02bc233122d9e0b4349abc0d2c437cb Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 01:11:54 +0200 Subject: [PATCH 194/203] fix(libs): real Vrr names for VideoOut pair, generic Unknown00/01 for the rest --- core/libs/prx/libSceVideoOut/src/Output.cpp | 25 ++++++++++++--------- docs/dev/TechnicalDebt.md | 4 +--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 74b011e8a..e01a2a8d8 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -276,29 +276,32 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } -APS5_EXPORT("T4ucGB8CsnM", sceVideoOutVrrPeg); -APS5_EXPORT("5tRaBjtdTzY", sceVideoOutVrrPeg); -int APS5_VABI sceVideoOutVrrPeg(std::uint64_t a0, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3) try { - (void)a0; - (void)a1; - (void)a2; - (void)a3; +APS5_EXPORT("T4ucGB8CsnM", sceVideoOutVrrUnpegFromFixedRate); +int APS5_VABI sceVideoOutVrrUnpegFromFixedRate() try { NotImplemented_nid_no_patch(__func__); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } -APS5_EXPORT("kP2L8t3j-aM", sceVideoOutVrrStatusUnknown00); -int APS5_VABI sceVideoOutVrrStatusUnknown00() try { +APS5_EXPORT("5tRaBjtdTzY", sceVideoOutVrrPegToFixedRate); +int APS5_VABI sceVideoOutVrrPegToFixedRate() try { NotImplemented_nid_no_patch(__func__); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } -APS5_EXPORT("LibwuIonIBw", sceVideoOutVrrStatusUnknown01); -int APS5_VABI sceVideoOutVrrStatusUnknown01() try { +APS5_EXPORT("kP2L8t3j-aM", sceVideoOutUnknown00); +int APS5_VABI sceVideoOutUnknown00() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +APS5_EXPORT("LibwuIonIBw", sceVideoOutUnknown01); +int APS5_VABI sceVideoOutUnknown01() try { NotImplemented_nid_no_patch(__func__); return 0; } catch (const ProcessShutdown&) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e6c2dc8f2..533515028 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -67,10 +67,8 @@ Throughout the project, every function at every stage either **does exactly what - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` - [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) +- [kP2L8t3j-aM](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, signature - [LibwuIonIBw](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, signature -- [T4ucGB8CsnM](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, takes four opaque 64-bit arguments -- [5tRaBjtdTzY](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, takes four opaque 64-bit arguments -- [kP2L8t3j-aM](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, takes no arguments - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From 9c5205973bd93315f1d3d6d670f700483ca517e7 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 01:13:22 +0200 Subject: [PATCH 195/203] fix(libs): export recomputing Vrr names without APS5_EXPORT alias --- core/libs/prx/libSceVideoOut/src/Output.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index e01a2a8d8..af435d9a3 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -276,7 +276,6 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } -APS5_EXPORT("T4ucGB8CsnM", sceVideoOutVrrUnpegFromFixedRate); int APS5_VABI sceVideoOutVrrUnpegFromFixedRate() try { NotImplemented_nid_no_patch(__func__); return 0; @@ -284,7 +283,6 @@ int APS5_VABI sceVideoOutVrrUnpegFromFixedRate() try { LibcAwaitExit_nid_postfix(); } -APS5_EXPORT("5tRaBjtdTzY", sceVideoOutVrrPegToFixedRate); int APS5_VABI sceVideoOutVrrPegToFixedRate() try { NotImplemented_nid_no_patch(__func__); return 0; From c25ff91a65c341636482c1766cb9801a8cdb3388 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 4 Oct 2026 21:03:20 +0200 Subject: [PATCH 196/203] fix(libkernel): accept GPU coherency prot bits for host mapping --- .../prx/libkernel/DirectMemory/DirectMemory.cpp | 2 +- core/libs/tests/GuestMemory.cpp | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 7c58e56f3..7678dd8ac 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -194,7 +194,7 @@ void ValidateRange(const void* addr, size_t len, size_t alignment) { } int LinuxProtFromSce(int prot) { - if ((prot & ~0xF7) != 0) { + if ((prot & ~0x3F7) != 0) { // return SCE_KERNEL_ERROR_EINVAL; throw std::invalid_argument("Unsupported memory protection bits: " + std::to_string(prot)); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 2fe9b8b06..f8bb5e35c 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -340,6 +340,18 @@ static void CheckMtypeprotect() { Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); } +static void CheckDirectMemoryGpuProtBits() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, page, 0x3f2, 0, phys, 0) == 0); + static_cast(mapping)[0] = 11; + Require(static_cast(mapping)[0] == 11); + Require(sceKernelMunmap(mapping, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + static void CheckFixedVirtualReservation() { constexpr std::size_t page = 0x4000; void* probe = nullptr; @@ -923,6 +935,7 @@ int main() { CheckCheckedReleaseDirectMemory(); CheckDirectMemoryFollowsPhysicalPages(); CheckReleaseDirectMemoryClearsMappings(); + CheckDirectMemoryGpuProtBits(); CheckFixedVirtualReservation(); CheckReservedRangeIsNotCommitted(); CheckNoOverwriteRefusesLiveMapping(); From 49b2f9e49b79bde418d884bbdcd0befb58a28d80 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 00:53:31 +0200 Subject: [PATCH 197/203] docs: record assumed semantics of prot bits 0x100/0x200 --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fbfddc202..7fd562073 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -186,6 +186,7 @@ Throughout the project, every function at every stage either **does exactly what - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. - [sceKernelReadv / sceKernelWritev / sceKernelPreadv / sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) are implemented on Linux only; on Windows they throw +- [LinuxProtFromSce](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) From c94949aedb3da191241508524ecf718271a02083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:22:43 +0200 Subject: [PATCH 198/203] feat(rtc): implement sceRtcGetDosTime and sceRtcSetDosTime Co-Authored-By: Claude --- core/libs/prx/libSceRtc/Export.cpp | 29 +++++++++++++++++++++++++++++ core/libs/tests/GuestRtc.cpp | 25 +++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 55 insertions(+) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index d5d542cf1..ee0f04f5c 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -369,6 +369,35 @@ int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32_time) { return 0; } +int APS5_VABI sceRtcGetDosTime(const RtcDateTime* time, uint32_t* dos_time) { + if (!dos_time) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + if (time->year < 1980 || time->year > 2107) { + NotImplemented_nid_no_patch(__func__); + return SCE_RTC_ERROR_INVALID_YEAR; + } + *dos_time = static_cast(time->year - 1980) << 25 | static_cast(time->month) << 21 + | static_cast(time->day) << 16 | static_cast(time->hour) << 11 + | static_cast(time->minute) << 5 | static_cast(time->second / 2); + return 0; +} + +int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dos_time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (dos_time >> 31 != 0) { + NotImplemented_nid_no_patch(__func__); + return SCE_RTC_ERROR_INVALID_VALUE; + } + time->year = static_cast(1980 + (dos_time >> 25)); + time->month = static_cast(dos_time >> 21 & 0x0f); + time->day = static_cast(dos_time >> 16 & 0x1f); + time->hour = static_cast(dos_time >> 11 & 0x1f); + time->minute = static_cast(dos_time >> 5 & 0x3f); + time->second = static_cast((dos_time & 0x1f) * 2); + time->microsecond = 0; + return 0; +} + int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index e81c0e44f..695feed8c 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -23,6 +23,8 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); int APS5_VABI sceRtcSetTime_t(RtcDateTime*, std::int64_t); int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t); +int APS5_VABI sceRtcGetDosTime(const RtcDateTime*, std::uint32_t*); +int APS5_VABI sceRtcSetDosTime(RtcDateTime*, std::uint32_t); int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int); int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*); int APS5_VABI sceRtcParseDateTime(RtcTick*, const char*); @@ -106,6 +108,29 @@ int main() { std::uint64_t fileTime = 0; Require(sceRtcGetWin32FileTime(&epoch, &fileTime) == 0 && fileTime == 116444736000000000ull); Require(sceRtcSetWin32FileTime(&converted, 116444736000000000ull) == 0 && Equal(converted, epoch)); + std::uint32_t dosTime = 0xffffffffu; + const RtcDateTime dosDate{2024, 2, 29, 12, 34, 57, 789000}; + Require(sceRtcGetDosTime(&dosDate, &dosTime) == 0 && dosTime == 0x585d645cu); + const RtcDateTime dosFirst{1980, 1, 1, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosFirst, &dosTime) == 0 && dosTime == 0x00210000u); + const RtcDateTime dosLast{2107, 12, 31, 23, 59, 59, 0}; + Require(sceRtcGetDosTime(&dosLast, &dosTime) == 0 && dosTime == 0xff9fbf7du); + Require(sceRtcGetDosTime(&dosDate, nullptr) == invalidPointer); + Require(sceRtcGetDosTime(nullptr, &dosTime) == invalidPointer); + const RtcDateTime dosBadMonth{2024, 13, 1, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosBadMonth, &dosTime) == invalidMonth); + const RtcDateTime dosEarly{1979, 12, 31, 0, 0, 0, 0}; + bool dosEarlyThrew = false; + try { sceRtcGetDosTime(&dosEarly, &dosTime); } catch (const std::exception&) { dosEarlyThrew = true; } + Require(dosEarlyThrew); + converted = RtcDateTime{1, 1, 1, 1, 1, 1, 1}; + Require(sceRtcSetDosTime(&converted, 0x585d645cu) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 12, 34, 56, 0})); + Require(sceRtcSetDosTime(&converted, 0x7f9fbf7du) == 0 && Equal(converted, RtcDateTime{2043, 12, 31, 23, 59, 58, 0})); + Require(sceRtcSetDosTime(&converted, 0) == 0 && Equal(converted, RtcDateTime{1980, 0, 0, 0, 0, 0, 0})); + Require(sceRtcSetDosTime(nullptr, 0) == invalidPointer); + bool dosHighThrew = false; + try { sceRtcSetDosTime(&converted, 0x80210000u); } catch (const std::exception&) { dosHighThrew = true; } + Require(dosHighThrew); char text[32]; tick.tick = leapDayTick; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fbfddc202..20cd80f75 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -22,6 +22,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer` and `sceSystemServiceReenableMusicPlayer` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify and no system music player to pause or resume - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values. +- [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). Years outside 1980-2107 (`GetDosTime`) and year fields above 63 (`SetDosTime`, where shadPS4 sign-extends the 16-bit date to years before 1980) throw, since no reference says what the library returns for them. `SetDosTime` does not validate the fields, as in shadPS4 - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. From d081e6749df4b3fa1e18b23d60fe31307a15f364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:38:51 +0200 Subject: [PATCH 199/203] feat(libkernel): sceKernelAddTimerEvent and sceKernelDeleteTimerEvent Co-Authored-By: Claude --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 29 +++++++- core/libs/prx/libkernel/Equeue/Equeue.hpp | 4 +- core/libs/tests/GuestEqueueEvents.cpp | 83 +++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 50b5c7faa..b328aecf5 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -46,7 +46,14 @@ void KernelEqueuePrivate::Close() { void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { for (auto& ev : m_events) { - if (!ev.triggered && ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { + if (ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { + if (ev.event.filter == EVFILT_TIMER) { + const uint64_t count = ev.intervalNs == 0 + ? (ev.triggered ? 0 : 1) + : 1 + (nowNs - ev.deadlineNs) / ev.intervalNs; + ev.event.data += static_cast(count); + ev.deadlineNs += count * ev.intervalNs; + } ev.triggered = true; } } @@ -176,7 +183,11 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { } ); if (it != m_events.end()) { + if (event.event.filter == EVFILT_TIMER) { + TriggerExpiredTimers(MonotonicNs()); + } it->deadlineNs = event.deadlineNs; + it->intervalNs = event.intervalNs; it->event.udata = event.event.udata; for (auto& pending : it->pendingEvents) { pending.udata = event.event.udata; @@ -403,6 +414,22 @@ int APS5_VABI sceKernelDeleteHRTimerEvent(KernelEqueue eq, int id) { return EqueueDeleteEvent_nid_postfix(eq, static_cast(id), EVFILT_HRTIMER); } +int APS5_VABI sceKernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata) { + const uint64_t intervalNs = static_cast(usec) * 1000ULL; + KernelEqueueEvent event{}; + event.deadlineNs = KernelEqueuePrivate::MonotonicNs() + intervalNs; + event.intervalNs = intervalNs; + event.event.ident = static_cast(id); + event.event.filter = EVFILT_TIMER; + event.event.flags = EV_ADD | EV_CLEAR; + event.event.udata = udata; + return EqueueAddEvent_nid_postfix(eq, event); +} + +int APS5_VABI sceKernelDeleteTimerEvent(KernelEqueue eq, int id) { + return EqueueDeleteEvent_nid_postfix(eq, static_cast(id), EVFILT_TIMER); +} + int APS5_VABI sceKernelAddAmprEvent(KernelEqueue eq, int id, void* udata) { if (eq == 0) { return EQUEUE_OK; diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 48085e6ac..c5d4f3e5d 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -14,6 +14,7 @@ #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +static constexpr int16_t EVFILT_TIMER = -7; static constexpr int16_t EVFILT_USER = -11; static constexpr int16_t EVFILT_VIDEO_OUT = -13; static constexpr int16_t EVFILT_HRTIMER = -15; @@ -42,6 +43,7 @@ struct KernelFilter { struct KernelEqueueEvent { bool triggered = false; uint64_t deadlineNs = 0; + uint64_t intervalNs = 0; KernelEvent event; KernelFilter filter; std::deque pendingEvents; @@ -64,9 +66,9 @@ public: int GetTriggeredEvents(KernelEvent* ev, int num); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); void Close(); + static uint64_t MonotonicNs(); private: - static uint64_t MonotonicNs(); void TriggerExpiredTimers(uint64_t nowNs); bool NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const; diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 4731605d0..e7fa6bddb 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -1,7 +1,9 @@ #include "SceTypes.hpp" +#include #include #include #include +#include extern "C" { int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); @@ -11,6 +13,8 @@ int APS5_VABI sceKernelAddUserEvent(KernelEqueue eq, int id); int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue eq, int id, void* udata); int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id); int APS5_VABI sceKernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTimespec* ts, void* udata); +int APS5_VABI sceKernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata); +int APS5_VABI sceKernelDeleteTimerEvent(KernelEqueue eq, int id); intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev); intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev); int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev); @@ -19,6 +23,10 @@ void* APS5_VABI sceKernelGetEventUserData(const KernelEvent* ev); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); +static constexpr int EVFILT_TIMER = -7; static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; @@ -34,7 +42,82 @@ static bool RejectsNull(TResult (APS5_VABI *accessor)(const KernelEvent*)) { return false; } +static void SleepAtLeast(std::chrono::steady_clock::duration duration) { + const auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < duration) { + std::this_thread::sleep_for(duration); + } +} + +static void VerifyPeriodicTimer() { + using namespace std::chrono_literals; + int first = 0; + int second = 0; + Require(sceKernelAddTimerEvent(0, 10, 1000, &first) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelDeleteTimerEvent(0, 10) == SCE_KERNEL_ERROR_EBADF); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "timer") == SCE_OK); + Require(sceKernelDeleteTimerEvent(eq, 10) == SCE_KERNEL_ERROR_ENOENT); + + KernelEvent events[2]{}; + int count = 0; + const KernelUseconds poll = 0; + const KernelUseconds wait = 1000000; + Require(sceKernelAddTimerEvent(eq, 10, 1000, &first) == SCE_OK); + SleepAtLeast(5ms); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1); + Require(sceKernelGetEventId(&events[0]) == 10); + Require(sceKernelGetEventFilter(&events[0]) == EVFILT_TIMER); + Require(sceKernelGetEventData(&events[0]) >= 5); + Require(sceKernelGetEventUserData(&events[0]) == &first); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &wait) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) >= 1); + + SleepAtLeast(5ms); + Require(sceKernelAddTimerEvent(eq, 10, 3000000000u, &second) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1); + Require(sceKernelGetEventData(&events[0]) >= 5); + Require(sceKernelGetEventUserData(&events[0]) == &second); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelDeleteTimerEvent(eq, 10) == SCE_OK); + Require(sceKernelDeleteTimerEvent(eq, 10) == SCE_KERNEL_ERROR_ENOENT); + + Require(sceKernelAddTimerEvent(eq, 13, 3000000000u, &first) == SCE_OK); + const auto readded = std::chrono::steady_clock::now(); + Require(sceKernelAddTimerEvent(eq, 13, 200000, &first) == SCE_OK); + SleepAtLeast(250ms); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) >= 1); + const intptr_t expirations = sceKernelGetEventData(&events[0]); + const int again = sceKernelWaitEqueue(eq, events, 2, &count, &poll); + if (std::chrono::steady_clock::now() - readded < 400ms) { + Require(expirations == 1 && again == SCE_KERNEL_ERROR_ETIMEDOUT); + } + Require(sceKernelWaitEqueue(eq, events, 2, &count, &wait) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 13); + Require(sceKernelDeleteTimerEvent(eq, 13) == SCE_OK); + + Require(sceKernelAddTimerEvent(eq, 11, 0, &first) == SCE_OK); + Require(sceKernelAddTimerEvent(eq, 11, 0, &first) == SCE_OK); + for (int i = 0; i < 2; ++i) { + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) == 1); + } + Require(sceKernelDeleteTimerEvent(eq, 11) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + + const KernelTimespec delay{0, 1000000}; + Require(sceKernelAddHRTimerEvent(eq, 12, &delay, &first) == SCE_OK); + Require(sceKernelDeleteTimerEvent(eq, 12) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { + VerifyPeriodicTimer(); + KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "events") == SCE_OK); const KernelUseconds timeout = 1000000; From c3871dcb99cf2f1556bb628584c65b73c3ee371c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 10:12:08 +0200 Subject: [PATCH 200/203] feat(libSceAudio3d): implement sceAudio3dObjectReserve and sceAudio3dObjectUnreserve --- core/libs/prx/libSceAudio3d/Export.cpp | 28 +++++++++++++++++++++ core/libs/tests/GuestAudio3dPort.cpp | 34 ++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 ++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index a31de1c48..5254ad3c7 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,12 +14,14 @@ namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); +constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); constexpr int AUDIO3D_USER_ID_SYSTEM = 0xFF; constexpr std::uint32_t AUDIO3D_PORT_ID = 0; +constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; @@ -36,6 +39,9 @@ struct Audio3dPort { Clock::duration frame_duration{}; std::uint32_t advanced = 0; std::deque playing; + std::uint32_t max_objects = 0; + std::uint32_t next_object_id = 0; + std::set objects; }; std::mutex g_mutex; @@ -136,6 +142,7 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port = Audio3dPort{}; g_port.open = true; g_port.queue_depth = parameters->queue_depth; + g_port.max_objects = parameters->max_objects; g_port.frame_duration = std::chrono::duration_cast( std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); *id = AUDIO3D_PORT_ID; @@ -179,4 +186,25 @@ int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id } } +int APS5_VABI sceAudio3dObjectReserve(uint32_t port_id, uint32_t* object_id) { + if (object_id == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + *object_id = AUDIO3D_OBJECT_INVALID; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (g_port.objects.size() >= g_port.max_objects) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + do { + ++g_port.next_object_id; + } while (g_port.next_object_id == 0 || g_port.next_object_id == AUDIO3D_OBJECT_INVALID || g_port.objects.contains(g_port.next_object_id)); + g_port.objects.insert(g_port.next_object_id); + *object_id = g_port.next_object_id; + return 0; +} + +int APS5_VABI sceAudio3dObjectUnreserve(uint32_t port_id, uint32_t object_id) { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (g_port.objects.erase(object_id) == 0) return AUDIO3D_ERROR_INVALID_OBJECT; + return 0; +} + } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 4eb2488a0..30b8c7d8a 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -16,15 +16,19 @@ int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t at int APS5_VABI sceAudio3dPortGetQueueLevel(std::uint32_t port_id, std::uint32_t* queue_level, std::uint32_t* queue_available); int APS5_VABI sceAudio3dPortAdvance(std::uint32_t port_id); int APS5_VABI sceAudio3dPortPush(std::uint32_t port_id, std::uint32_t blocking); +int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id); +int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id); } namespace { constexpr int INVALID_PORT = static_cast(0x80EA0002); +constexpr int INVALID_OBJECT = static_cast(0x80EA0003); constexpr int INVALID_PARAMETER = static_cast(0x80EA0004); constexpr int OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int NOT_READY = static_cast(0x80EA0007); constexpr int SYSTEM_USER = 0xFF; +constexpr std::uint32_t OBJECT_INVALID = 0xFFFFFFFF; void Require(bool value, const char* message) { if (value) return; @@ -140,6 +144,35 @@ void CheckAttributes() { RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10001, &value, 8); }, "wrong attribute size must throw"); } +void CheckObjects() { + std::uint32_t object = 7; + Require(sceAudio3dObjectReserve(0, nullptr) == INVALID_PARAMETER, "null object id"); + Require(sceAudio3dObjectReserve(0, &object) == INVALID_PORT && object == OBJECT_INVALID, "reserve needs an open port"); + Require(sceAudio3dObjectUnreserve(0, 1) == INVALID_PORT, "unreserve needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + parameters.max_objects = 2; + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + object = 7; + Require(sceAudio3dObjectReserve(1, &object) == INVALID_PORT && object == OBJECT_INVALID, "reserve on port 1"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 1, "first object is 1"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 2, "second object is 2"); + Require(sceAudio3dObjectReserve(0, &object) == OUT_OF_RESOURCES && object == OBJECT_INVALID, "max_objects is the limit"); + Require(sceAudio3dObjectUnreserve(1, 1) == INVALID_PORT, "unreserve on port 1"); + Require(sceAudio3dObjectUnreserve(0, 3) == INVALID_OBJECT, "unreserve of an unknown object"); + Require(sceAudio3dObjectUnreserve(0, OBJECT_INVALID) == INVALID_OBJECT, "unreserve of the invalid object"); + Require(sceAudio3dObjectUnreserve(0, 1) == 0, "unreserve"); + Require(sceAudio3dObjectUnreserve(0, 1) == INVALID_OBJECT, "double unreserve"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 3, "ids are not reused while the port is open"); + Require(sceAudio3dObjectReserve(0, &object) == OUT_OF_RESOURCES, "full again"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(Open(parameters, &id) == 0 && id == 0, "reopen"); + Require(sceAudio3dObjectUnreserve(0, 2) == INVALID_OBJECT, "close drops the objects"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 1, "ids restart on a new port"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 2, "a new port has the full limit"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckQueue() { using namespace std::chrono; constexpr std::uint32_t granularity = 0x1800; @@ -201,6 +234,7 @@ int main() { CheckBeforeInitialize(); CheckOpenParameters(); CheckOpenClose(); + CheckObjects(); CheckQueue(); CheckTerminate(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9a5bbf0d2..9af92c65f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -22,7 +22,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer` and `sceSystemServiceReenableMusicPlayer` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify and no system music player to pause or resume - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values. -- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them +- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved but the library has no bed exports or `sceAudio3dObjectSetAttributes` to give them audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. @@ -47,6 +47,7 @@ Throughout the project, every function at every stage either **does exactly what - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`), plus the RFC 2822 shape `Ddd, DD Mon YYYY HH:MM:SS[ GMT| ±HHMM]` and the asctime shape `Ddd Mon DD HH:MM:SS YYYY` (day zero- or space-padded) that shadPS4 parses at fixed positions; the weekday name is not checked against the date, a `±HHMM` offset is subtracted to get UTC like the RFC 3339 path (shadPS4 adds it), a string without a zone is taken as UTC, and anything outside these shapes throws instead of returning a parse error, unverified against hardware - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too +- [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR counter access](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded From 0781225d4314894ea90225e8db8f9b9e91f045c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 11:50:22 +0200 Subject: [PATCH 201/203] fix(rtc): decode the DOS year as unsigned, move TechnicalDebt line Co-Authored-By: Claude --- core/libs/prx/libSceRtc/Export.cpp | 4 ---- core/libs/tests/GuestRtc.cpp | 5 ++--- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index ee0f04f5c..a8986f228 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -384,10 +384,6 @@ int APS5_VABI sceRtcGetDosTime(const RtcDateTime* time, uint32_t* dos_time) { int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dos_time) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (dos_time >> 31 != 0) { - NotImplemented_nid_no_patch(__func__); - return SCE_RTC_ERROR_INVALID_VALUE; - } time->year = static_cast(1980 + (dos_time >> 25)); time->month = static_cast(dos_time >> 21 & 0x0f); time->day = static_cast(dos_time >> 16 & 0x1f); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 695feed8c..576dacbb8 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -128,9 +128,8 @@ int main() { Require(sceRtcSetDosTime(&converted, 0x7f9fbf7du) == 0 && Equal(converted, RtcDateTime{2043, 12, 31, 23, 59, 58, 0})); Require(sceRtcSetDosTime(&converted, 0) == 0 && Equal(converted, RtcDateTime{1980, 0, 0, 0, 0, 0, 0})); Require(sceRtcSetDosTime(nullptr, 0) == invalidPointer); - bool dosHighThrew = false; - try { sceRtcSetDosTime(&converted, 0x80210000u); } catch (const std::exception&) { dosHighThrew = true; } - Require(dosHighThrew); + Require(sceRtcSetDosTime(&converted, 0xff9fbf7du) == 0 && Equal(converted, RtcDateTime{2107, 12, 31, 23, 59, 58, 0})); + Require(sceRtcSetDosTime(&converted, 0x80210000u) == 0 && Equal(converted, RtcDateTime{2044, 1, 1, 0, 0, 0, 0})); char text[32]; tick.tick = leapDayTick; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 20cd80f75..e31013ca8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -22,7 +22,6 @@ Throughout the project, every function at every stage either **does exactly what - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer` and `sceSystemServiceReenableMusicPlayer` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify and no system music player to pause or resume - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values. -- [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). Years outside 1980-2107 (`GetDosTime`) and year fields above 63 (`SetDosTime`, where shadPS4 sign-extends the 16-bit date to years before 1980) throw, since no reference says what the library returns for them. `SetDosTime` does not validate the fields, as in shadPS4 - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. @@ -192,6 +191,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown +- [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). Years outside 1980-2107 throw in `GetDosTime`, since no reference says what the library returns for them. `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107), where shadPS4 sign-extends the 16-bit date to years before 1980. `SetDosTime` does not validate the fields, as in shadPS4 - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty From 9ab637a5759af38b173d9936e16a701c8975cf7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 11:50:39 +0200 Subject: [PATCH 202/203] fix(libkernel): wake infinite equeue waits for timers, document timer choices Co-Authored-By: Claude --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 7 ++++++- core/libs/tests/GuestEqueueEvents.cpp | 9 +++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index b328aecf5..7d4769d6e 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -158,7 +158,12 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros return SCE_KERNEL_ERROR_EBADF; } if (micros == 0) { - m_cond.Wait(lock); + uint32_t timerWait = 0; + if (NextTimerWaitMicros(MonotonicNs(), &timerWait)) { + m_cond.WaitUntil(lock, TimedWait::NowNanos() + static_cast(timerWait) * 1000ULL); + } else { + m_cond.Wait(lock); + } } else { uint32_t timerWait = 0; const bool hasTimer = NextTimerWaitMicros(MonotonicNs(), &timerWait); diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index e7fa6bddb..b0f74d8cc 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -100,6 +100,15 @@ static void VerifyPeriodicTimer() { Require(count == 1 && sceKernelGetEventId(&events[0]) == 13); Require(sceKernelDeleteTimerEvent(eq, 13) == SCE_OK); + Require(sceKernelAddTimerEvent(eq, 14, 1000, &first) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, nullptr) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 14 && sceKernelGetEventData(&events[0]) >= 1); + Require(sceKernelDeleteTimerEvent(eq, 14) == SCE_OK); + const KernelTimespec soon{0, 1000000}; + Require(sceKernelAddHRTimerEvent(eq, 15, &soon, &second) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, nullptr) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 15 && sceKernelGetEventFilter(&events[0]) == EVFILT_HRTIMER); + Require(sceKernelAddTimerEvent(eq, 11, 0, &first) == SCE_OK); Require(sceKernelAddTimerEvent(eq, 11, 0, &first) == SCE_OK); for (int i = 0; i < 2; ++i) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9a5bbf0d2..96c96f487 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -184,6 +184,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVdecswResetDecoder](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature - [sceCoredumpAttachMemoryRegion](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature +- [sceKernelAddTimerEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. From 5b331ed02243a60989db45db93813009c8ff379e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 10:21:14 +0200 Subject: [PATCH 203/203] feat(libSceAppContent): implement sceAppContentGetAddcontInfo and GetAddcontInfoList Co-Authored-By: Claude --- core/libs/prx/libSceAppContent/Export.cpp | 19 ++++++++++++++ core/libs/tests/GuestAppContent.cpp | 30 +++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index dfc8300ad..3a025d498 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -11,6 +11,7 @@ static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast(0x80D90002); static constexpr int SCE_APP_CONTENT_ERROR_NOT_FOUND = static_cast(0x80D90005); +static constexpr int SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT = static_cast(0x80D90007); static constexpr uint32_t APPPARAM_ID_SKU_FLAG = 1; static constexpr int32_t SKU_FLAG_FULL = 3; @@ -18,6 +19,11 @@ static constexpr char TEMPORARY_MOUNT_POINT[] = "/temp0"; static constexpr char DOWNLOAD_MOUNT_POINT[] = "/download0"; static constexpr uint32_t TEMPORARY_DATA_OPTION_FORMAT = 1; +struct AppContentAddcontInfo { + NpUnifiedEntitlementLabel entitlement_label; + uint32_t status; +}; + static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) { if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); @@ -40,6 +46,19 @@ int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint* mount_poin return SCE_APP_CONTENT_ERROR_NOT_FOUND; } +int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentAddcontInfo* info) { + (void)service_label; + if (!entitlement_label || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; +} + +int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t service_label, AppContentAddcontInfo* list, uint32_t list_num, uint32_t* hit_num) { + (void)service_label; + if ((!list || list_num == 0) && !hit_num) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (hit_num) *hit_num = 0; + return 0; +} + int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { if (!value) return SCE_APP_CONTENT_ERROR_PARAMETER; switch (param_id) { diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index d78335075..67b4a6c68 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -5,10 +5,13 @@ extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*); int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); +int APS5_VABI sceAppContentGetAddcontInfo(uint32_t, const NpUnifiedEntitlementLabel*, void*); +int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t, void*, uint32_t, uint32_t*); } static constexpr int ErrorParameter = static_cast(0x80D90002); static constexpr int ErrorNotFound = static_cast(0x80D90005); +static constexpr int ErrorDrmNoEntitlement = static_cast(0x80D90007); static void Require(bool value) { if (!value) std::abort(); } int main() { @@ -24,4 +27,31 @@ int main() { std::memcpy(mountPoint.data, "/addcont0", 10); Require(sceAppContentAddcontUnmount(&mountPoint) == ErrorNotFound); Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); + + unsigned char info[24]; + std::memset(info, 0x5a, sizeof(info)); + unsigned char untouchedInfo[24]; + std::memcpy(untouchedInfo, info, sizeof(info)); + Require(sceAppContentGetAddcontInfo(0, &label, info) == ErrorDrmNoEntitlement); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfo(0, nullptr, info) == ErrorParameter); + Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + + uint32_t hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, info, 0, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, info, 1, &hitNum) == 0); + Require(hitNum == 0); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfoList(0, info, 1, nullptr) == 0); + Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoList(0, info, 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9a5bbf0d2..073e574f6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -200,7 +200,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported -- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out +- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler but nothing delivers to it: a host fault still ends the process, and `sceKernelRaiseException` is not implemented. PPSA12544's PS5Util module (Unity) installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection, so delivery must run the handler on the target thread with a FreeBSD `ucontext` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used